OpenNel Manual
- OpenNel Manual
- Forward
- About The Author
- Acknowledgments
- Introduction
- Who This Book Is For
- How This Book Is Structured
- Prerequisites
- Downloading The Code
- Contacting The Author
- Getting Started
- What Is NeL
- NeL Features
- Origins Of NeL
- Installing NeL
- Building NeL From Source
- Quick Start Introduction
- Modules: NLMISC
- Basics
- Handling Command Line Options
- Using Logging
- Using Config Files
- The Config File Code
- Retrieving Config File Values
- Working With Config File Arrays
- Setting Config File Variables
- Config File Callbacks
- Miscellaneous Config File Options
- The Config File Format
- Basic Debugging
- String Utilities
- Internationalization
- File and Path Management
- Using Path and File Utilities
- Input and Output
- Creating and Using Big Files
- Creating and Using XML Packs
- Time Tools
- Advanced
- Modules: NL3D
- Basics
- The Driver
- Working with Fonts
- Managing the Scene
- Manipulating the Camera
- Working with Shapes
- Working with Skeletons
- Managing and Playing Animations
- Advanced
- Modules: NLNET
- Modules: NLPACS
- Modules: NLGEORGES
- Modules: NLLIGO
- NeL Network Services (NeLNS)
- Overview
- Login Service (LS)
- Welcome Service (WS)
- Naming Service (NS)
- Frontend Service (FS)
- Admin Services
- Tools
Forward
About The Author
Matt Raykowski has been involved in the NeL community since early 2002 and has a broad background of experiencing in many roles including network engineering, system administration, technical support, application development and management. Matt is currently the technical supervisor of corporate application development for Children's Hospitals and Clinics of Minnesota. In 2001 Matt and his intrepid group who later took on the name Ironic Entertainment began designing a game called Werewolf and began searching for a suitable library or set of libraries to meet their needs - their ultimate choice was NeL. Matt is still the lead programmer for the Werewolf project as well as a primary contact for the NeL community.
Acknowledgments
Introduction
So you picked this book up. Maybe you're wondering what NeL is? Or maybe you're a long time user that just needs a good reference. Either way this book will meet your needs. We'll start out with some discussion about what NeL is, what kind of features it provides, where it came from and why you should use it. Following that you will see a per-module breakdown of functionality, how to use it and how it inter-operates with the rest of the system.
Who This Book Is For
This book is for experienced programmers who are interested in leverage one or more of the modules that NeL provides. Your experience in NeL may vary from none to experienced user - this book will provide useful knowledge to anyone using NeL.
How This Book Is Structured
The book will begin with a quick chapter on getting started with NeL which will provide a quick start and a brief taste in coding with the NeL framework. Following that the book will be broken into sections dedicated to each of the modules and tools.
Prerequisites
You should be familiar with ISO Standard C++ and the C++ Standard Template Library (STL.) You should possess a copy of and be familiar with a common compiler for one of the platforms supported by NeL.
- On Windows: Microsoft Visual Studio .NET 2003, 2005 or 2005 Express.
- On Linux (2.6.x, any distribution): GCC 4.0.2 or above. KDevelop is helpful.
- On Mac OS X (Tiger or Panther): a configured and working Xcode environment.
- On All Platforms: CMake 2.4.6 or newer.
Downloading The Code
You can find the code used in this book on the NeL website: www.opennel.org
For more information on NeL, tips and tricks, or simply to converse with other NeL users please refer to that site for resources such as forums and IRC.
Contacting The Author
The author may be contacted through his email address matt.raykowski@gmail.com
, you may also try contacting him on IRC (irc.freenode.net) in the #nel channel or on the OpenNeL forums as "sfb."
Getting Started
What Is NeL
NeL Features
Origins Of NeL
Installing NeL
Installing NeL On Windows
Presently there is no installer for Windows.
Installing NeL On Linux
On Debian and Debian-derived distributions run the following command:
sudo apt-get install libnel0 libnel-dev
The libnel-dev package is only required if you will be developing using NeL. If you only need the NeL runtime you will only need to install the libnel0 package. Also there is libnel-dbg which contains the debugging symbols, however it is very large.
Building NeL From Source
Building NeL On Windows
Checking Out the Source
When working with NeL on Windows you will want to check it out first since most of your required packages will be stored within the NeL source tree, a process that will be explained in the next section. An in depth explanation of source control management is outside of this scope but a brief explanation is required. Our source is divided primarily into three sections: trunk, branches, and tags. Trunk is our "working" tree of code. It is typically the most recent code and is what most developers using NeL (but not necessarily developing NeL) will use. Branches contain variations of trunk from some point in time. NeL framework developers use branches to work on large features that may impact the stability or the ability to compile code over a prolonged period of time. Tags are used to mark points in time of importance, typically we use tags to mark the source code used for a given feature release, e.g. tags/NEL_0_6_0 would be the 0.6.0 version release. To check out the source code you will need some Windows-based Subversion client such as TortoiseSVN, which is the preferred client of the Windows using NeL community. Once you have this tool installed you will use it to check out NeL.
If you have Cygwin or a similar environment installed do not use the svn client provided there if you intend on using Microsoft Visual Studio.
A common practice is to create a folder called "Projects" that you will use to hold your project source code. You can check out variations of the NeL source and your own project's source to this directory. While in this directory right click in the folder view and choose SVN Checkout...

Once this window opens you will fill out the URL of Repository field and the directory to which you would like NeL checked out.
- URL of Repository: https://nel.svn.sourceforge.net/svnroot/nel/trunk

- Checkout Directory: C:\Projects\nel-trunk
Note that we specified the directory nel-trunk - this is the directory you want the contents of the NeL subversion checked out to. Be aware of what directory you specify here.

Once you accept these settings it will begin the checkout process. It may warn you that the directory does not exist - this is fine, just click Yes to create the new directory.

The structure of our Subversion repository as of writing this is that all "components" of the NeL framework are immediately under trunk, then each component is broken out within it's respective portion. The components currently provided are nel the actual libraries, nelns the networking system, snowballs2 the technology demo and media which contains the original media (texture maps and native modeller files, e.g. .max files) used for NeL samples and demos.
Installing Required Packages
NeL requires a variety of packages to build and to run. For ease the NeL community maintains a large archive of the majority of required libraries. NeL requires the following libraries: freetype, jpeg, libXML, zlib, OpenGL, DirectX, OpenAL, FMOD, EAX, and potentially more libraries depending on the components you choose to compile. An indepth list of dependencies is provided on the OpenNeL website. The website also provides a link to an archive of dependencies. Download the file external8_date.rar from the OpenNeL SourceForge download site under External Dependencies. Extract this to C:\Projects\nel-trunk\3rdParty and the CMake build system will automatically include these paths in your build. This will meet most of your needs. 
Additionally you will need to get the latest DirectX 9 SDK from the Microsoft website if you want to build the Direct3D driver (the default behavior.)
If you are using one of the Express editions of Microsoft Visual Studio you will have to install the PlatformSDK before proceeding.
Building and Compiling NeL
Before you can begin compiling NeL you will first have to use CMake to generate the build environment. NeL forces developers to build out of source to prevent compilation and build environment changes from cluttering up the source tree. This makes it easier to manage changes and source commits, among a variety of benefits. So you will want to navigate to C:\Projects\nel-trunk\nel folder and create a new folder called cmake. Now start the CMake tool and choose the source directory and binary directory:
- Source Directory: C:\Projects\nel-trunk\nel
- Binary Directory: C:\Projects\nel-trunk\nel\cmake

Once you have set these you will first click Configure and choose the appropriate generator. In this case we will be building for Microsoft Visual Studio 8 2005. It may prompt you to create the build directories, this is normal.

dd
Building NeL On Linux
To build NeL on Linux you will first need to install some required and optional packages. Since this book cannot cover all the various Linux distributions and package management systems it will refer to the distribution most commonly used by the author: Ubuntu.
Installing Required Packages
You will need to install a series of packages to begin compiling NeL.
sudo apt-get install g++ subversion cmake libxml2 libxml2-dev libfreetype6 libfreetype6-dev libjpeg-dev xorg-dev x11proto-xf86vidmode-dev
In many cases you will already have these libraries and tools installed. A quick summary of what we installed is the compiler, a client to get the source, the tool to generate the build files, an XML parser and a variety of libraries used by the 3D system. Theoretically the 3D-related stuff is optional as you do not have to build every module but there will rarely be an instance where you will want only one module built.
We make the assumption that you have GL compliant driver installed. If not and you receive errors that certain GL-related headers are missing or CMake complaints that OPENGL_INCLUDE_DIR is missing then you may want to install the software-renderer Mesa drivers:
sudo apt-get install libgl1-mesa-dev
Installing Optional Packages
There are a few packages that are considered optional but are suggested to be installed anyway.
sudo apt-get install libopenal0a libopenal-dev libalut-dev libgtk2.0-dev libglade2-dev libglade2-0
What we are installing is the packages necessary for the OpenAL sound system and a completely optional GTK library so that they NeLNS daemons and other services will run in a GUI rather than as a crude console application.
Checking Out the Source
The Subversion tool we installed earlier is what we will use to get the source code. An in depth explanation of source control management is outside of this scope but a brief explanation is required. Our source is divided primarily into three sections: trunk, branches, and tags. Trunk is our "working" tree of code. It is typically the most recent code and is what most developers using NeL (but not necessarily developing NeL) will use. Branches contain variations of trunk from some point in time. NeL framework developers use branches to work on large features that may impact the stability or the ability to compile code over a prolonged period of time. Tags are used to mark points in time of importance, typically we use tags to mark the source code used for a given feature release, e.g. tags/NEL_0_6_0 would be the 0.6.0 version release. Here is how you check out the code:
$ pwd /home/me/sandbox $ svn co https://nel.svn.sourceforge.net/svnroot/nel/trunk nel-trunk [omitting svn output] $ ls nel-trunk
The structure of our Subversion repository as of writing this is that all "components" of the NeL framework are immediately under trunk, then each component is broken out within it's respective portion. The components currently provided are nel the actual libraries, nelns the networking system, snowballs2 the technology demo and media which contains the original media (texture maps and native modeller files, e.g. .max files) used for NeL samples and demos.
Building and Compiling NeL
For the purpose of this section we will focus on the NeL library. The steps performed here can more or less be performed verbatim on the other modules. In order to compile NeL we will first need to generate the build environment. The OpenNeL projects prefers that you use the CMake environment instead of the autotools and MSVC environment files delivered. Reliance on these files is not suggested as they will be deprecated and finally deleted soon. Our CMake configurations requires that you build out of source so that your build files do not litter the source tree. This makes development and management of source changes in NeL much easier and more convenient. So assuming the path used earlier we will need to move to the NeL source directory, create a directory to build in which we will call cmake for simplicity and finally we will build.
$ cd /home/me/sandbox/nel-trunk/nel $ mkdir cmake $ cd cmake $ cmake -G "Unix Makefiles" .. -- Looking for XOpenDisplay in /usr/lib/libX11.so;/usr/lib/libXext.so -- Looking for XOpenDisplay in /usr/lib/libX11.so;/usr/lib/libXext.so - found -- Looking for gethostbyname -- Looking for gethostbyname - found -- Looking for connect -- Looking for connect - found -- Looking for remove -- Looking for remove - found -- Looking for shmat -- Looking for shmat - found -- Looking for IceConnectionNumber in ICE -- Looking for IceConnectionNumber in ICE - found -- Configuring done -- Generating done -- Build files have been written to: /home/me/sandbox/nel-trunk/nel/cmake
NeL also has a number of configurable options you can toggle. For example if you are building a server and have no need for the 3D libraries you could simply disable compilation of them. NeL also provides some optional support which is not by default compiled such as GTK displayers for non-Windows environments. A common scenario is to change the installation prefix, which defaults to /usr/local - you may want to install it to your home directory such as /home/me/nel - here's an example of toggling some optional support and changing the installation prefix:
$ cmake -G "Unix Makefiles" -DCMAKE_INSTALL_PREFIX=/home/mattr/nel -DWITH_GTK=ON ..
Finally you will need to build NeL, which is a simple make command:
$ make Scanning dependencies of target nelmisc [ 0%] Building CXX object src/misc/CMakeFiles/nelmisc.dir/event_emitter.o [ 0%] Building CXX object src/misc/CMakeFiles/nelmisc.dir/eval_num_expr.o [ 0%] Building CXX object src/misc/CMakeFiles/nelmisc.dir/window_displayer.o
Note that CMake obscures the background commands to clean up the compiler output. It will output compiler-generated warnings and errors but if you need to see the actual command that is being executed you will need to run make in verbose mode:
$ make VERBOSE=1
Quick Start Introduction
Modules: NLMISC
Basics
Handling Command Line Options
(TBD, code needs to be finished)
Using Logging
The debugging system allows to log information at various levels. To get started you will want to include the header file "nel/misc/debug.h". This header contains all of the macros necessary to begin logging. The logging facilities provide five different logging levels: Error, Warning, Info, Debug and Assert. Each logging level can be accessed using it's associated logging macro. Each of these will take an expression (denoted by exp in the table below) and this expression is typically in the format that is passed to function printf.
| Logging Method |
Log Level |
Description |
|---|---|---|
| nlerror(exp) | Error | This method always logs to ErrorLog. Once completed logging it will throw the EFatalError exception. This must be in a try/catch block. |
| nlerrornoex(exp) | Error | This method always logs to ErrorLog. Unlike nlerror, this method does not throw an exception. It simply exits the application. This is typically used in a catch block. |
| nlwarning(exp) | Warning | In RELEASE mode this method does nothing. In all other modes this method logs to the WarningLog. The parameter exp is in printf syntax. |
| nlinfo(exp) | Info | In RELEASE mode this method does nothing. In all other modes this method logs to the InfoLog. The parameter exp is in printf syntax. |
| nldebug(exp) | Debug | In RELEASE mode this method does nothing. In all other modes this method logs to the DebugLog. The parameter exp is in printf syntax. |
| nlassert(exp) | Assert | In RELEASE mode this does nothing. In all other modes if the expression is false, it will log and then abort(). |
| nlassertonce(exp) | Assert | In RELEASE mode this does nothing. In all other modes if the expression is false, it will log and then abort(). This will only execute once in a loop. |
| nlassertex(exp,str) | Assert | In RELEASE mode this does nothing. In all other modes if the expression is false, it will log the parameter str and then abort(). |
| nlverify(exp) | Assert | In RELEASE mode this executes exp and nothing more. Otherwise this is the same as nlassert. |
| nlverifyonce(exp) | Assert |
In RELEASE mode this executes exp and nothing more. Otherwise this is the same as nlassertonce. |
| nlverifyex(exp,str) | Assert | In RELEASE mode this executes exp and nothing more. Otherwise this is the same as nlassertex. |
As you can see above these methods allow you to display strings variable arguments such as if you were using the printf function. In the default behavior, the text passed to these macros is displayed into STDOUT except in RELEASE mode. The text logged is also written into a file called "log.log" in the working directory. These macros print the strings with an information header.
Example :
sint32 age = -2; nldebug( "Toto is %d years old", age ); if ( age < 0 ) { nlerror( "Invalid age for toto : %d", age ); // the program will not come here because nlerror throws an exception to exit }
STDOUT (logger thread_id file line: debug_string):
DBG 1234 myfile.cpp 10: Toto is -2 years old
ERR 1234 myfile.cpp 13: Invalid age for toto : -2
Abort
FILE OUTPUT (date time logger debug_string):
01/04/11 18:24:50 DBG: Toto is -2 years old
01/04/11 18:24:50 ERR: Invalid age for toto : -2
Because NeL allows you to create multi-threaded programs these macros use mutual exclusions (mutex) to ensure no data is corrupted and the displayed text not interlaced. You will become very familiar with using the nlinfo macro as it is the most commonly used macro.
Using Config Files
Config files in NeL provide you with a powerful way to set up your programs at runtime. There are two parts to working with config files: the config file format and the CConfigFile class. There is a fantastic sample for this, so feel free to look in the samples directory for more hands on information. NeL makes working with config files a breeze so we'll cover the code portion first.
The Config File Code
Retrieving Config File Values
Most of the work done on a config file will be done through the CConfigFile class.
CConfigFile cf; cf.load("myconfig.cfg"); int var1 = cf.getVar("var1").asInt();
Retrieving variables from the config file is almost that simple. The config file system allows you to convert config file values to a variety of types:
| Type | Conversion Method |
|---|---|
| int | asInt() |
| double | asDouble() |
| float | asFloat() |
| string | asString() |
| bool | asBool() |
Working With Config File Arrays
This covers the essentials of working with config files but there's a whole lot more to config files. You can work with arrays in config files as well. When a parameter in the config file is declared as an array you can access it using the size method and then the asInt(int) method. You're not locked in to asInt, though, you can use all of the type conversion methods - the int parameter is what is important. Here is an example of accessing an array from the config file:
for(int i=0 ; i< cf.getVar("myArray").size() ; i++) { nlinfo("this is %d member of the array: %s", i, cf.getVar("myArray").asString(i)); }
You can see above that I was able to get the "myArray" variable and convert a specific member of it to the string type.
Something that is important to note: if you try to get a variable that doesn't exist or convert it to a type that NeL cannot convert it to (such as trying to convert a string to an integer) the config file system will throw the exception EConfigFile. If you are loading config files that may be changed or accessing variables that may not be in the config file it is highly recommended that you place you getVar calls within a try/catch block to compensate for potential config file errors.
Setting Config File Variables
The config file system allows you to update existing config files. The benefit to this is that you can update or fix config files from within your code automatically. There is only one downside to writing a config file from code: any formatting that you have in the config file and any comments you have are completely lost. With that said, here is a quick example of how you can set a config file variable and save it:
// Set a variable with an integer value. cf.getVar("myVariable").setAsInt(65536); // Create an array or strings. std::vector<std::string> foo; foo.push_back("test"); foo.push_back("bar"); cf.getVar("myArray").setAsString(foo); // Call this method to save the config file: cf.save();
Config File Callbacks
One last thing that config files allow you to do is monitor whether a file or variable has changed. This is useful if you want to drive runtime activity from the file or automatically adopt changes from the config file. The config file system provides two ways of monitoring changes to the config file system - by config file or by config file variable. To implement these you will need to create a function that can be called by the config file system in the event of a change.
By Config File
void myCallbackName()
By Config File Variable
void myVariableCallbackName(CConfigFile::CVar &var)
Once you have defined the methods you are interested in you simply need to register the callback with the config file system:
// For config file wide changes cf.setCallback(myCallbackName); // For config file variable specific changes cf.setCallback("myVariable", myVariableCallbackName);
Miscellaneous Config File Options
The config file class also has a few options to help you out. One of the handiest ones is the UnknownVariables public member. This vector of strings lists all of the variables that were requested but not found in the config file. You can use this method during troubleshooting to see if you have a misconfigured config file or to see what variables your code is calling that are not found. This can be extremely handy for finding typos within your code.
Also handy for troubleshooting is the display() public method. Call this method to make the config file system print out all of the variables that it loaded to to the InfoLog.
The Config File Format
The config file format uses C++ style comments:
// This is a comment in the config file. /* * And so is this! */
Really the format for a config file is pretty straight forward. Here is a good example:
var1 = 123; // var1 type:int, value:123
var2 = "456.25"; // var2 type:string, value:"456.25"
var3 = 123.123; // var3 type:real, value:123.123
// the resulting type is type of the first left value
var4 = 123.123 + 2; // var4 type:real, value:125.123
var5 = 123 + 2.1; // var5 type:int, value:125
var6 = (-112+1) * 3 - 14; // var6 type:int, value:-347
var7 = var1 + 1; // var7 type:int, value:124
var8 = var2 + 10; // var8 type:string, value:456.2510 (convert 10 into a string and concat it)
var9 = 10.15 + var2; // var9 type:real, value:466.40 (convert var2 into a real and add it)
var10 = { 10.0, 51.1 }; // var10 type:realarray, values:{10.0,51.1}
var11 = { "str1", "str2", "str3" }; // var11 type:stringarray, values:{"str1", "str2", "str3"}
var12 = { 133, -1 }; // var12 type:intarray, values:{133,-1}
var13 = { 1, 0.5, -1 };
Basic Debugging
In addition to the logging facilities NeL provides some basic debugging tools and the basis of some exception handling. There are two main debugging macros and one exception that are useful in every day development: nlstop, nlerror() and the EFatalError exception. You can also find an informative sample called debug in the OpenNeL source tree that covers this subject as well as some basic logging.
The nlstop macro is the most basic of the two. It requires no parameters and simply stops the code at that point. The nlstop macro perform no login in non-Debug build modes but it is important to be familiar with it and use it when building in Debug modes such as Debug or ReleaseDebug. In the Microsoft IDEs the macro will opt to set a breakpoint in the code and exit you if you are running within the IDE. This is a great way to programmatically set breakpoints for failure code rather than a simple error. The nlstop macro does throw the EFatalException exception, but unlike nlerror() (which will be covered next) it does not allow you to provide any contextual information.
The nlerror() macro is a useful variation in that you can use it immediately halt execution of the program but still provide useful, context sensitive logging. Because nlerror() calls abort() it should be used sparingly and only in situations where continued execution of code could cause problems, predictable crashes and other things of that nature. An important feature of nlerror() is that it emits an EFatalException exception that you can catch. This is tremendously useful for any cleanup that may be necessary upon a fatal error. Here's an example:
// when the program failed, call nlerror(), it displays the message and throws a EFatalError to // exit the program. don't forget to put a try/catch block everywhere an nlerror could // occurs. (In Visual Studio press F5 to continue) try { nlerror("nlerror() %d", 4); } catch(EFatalError &) { // just continue... nlinfo ("nlerror() generated an EFatalError exception, clean up and continue."); }
String Utilities
The NeL framework provides a wealth of string utilities for string manipulation and conversion. Four of the most important features are the toString and fromString methods as well as the CSString and ucstring classes. There are also many useful utility functions that we will also cover.
String Conversion
The string conversion utility really comes down to two components in NeL: a series of overloaded functions called toString() and fromString(). The names of these functions is pretty self-explanatory - the toString() functions convert a parameter to a string value and the fromString() functions convert a string to their native value. Listing all of the available overloads would take too much space but here is a quick demonstration using the nlinfo() logging facility.
void stringConversion
{
// Used to demonstrate integer conversion.
int myInt = 10;
std::string myStrInt = "15";
// Convert the integer to a string and log it.
nlinfo("myInt's current value is: %s", NLMISC::toString(myInt).c_str());
// The above will log: myInt's current value is: 10
// Now convert the string into the integer and then log it again.
myInt = NLMISC::fromString(myStrInt);
nlinfo("myInt's new value is: %s", NLMISC::toString(myInt).c_str());
// The above will log: myInt's new value is: 15
}
You can see how simple it was to convert a primitive into a string for logging or debugging. But you could also use these facilities in other practical applications - especially the fromString() functions. You could use this to parse user input into native primitive members or variables for use in game logic, for example.
The CSString Class
The CSString class is a very useful string utility class. It provides a wealth of utility functions too numerous to name all here and explain each of there uses. It would be better to quickly peruse the API documentation to see all of the methods available. A quick summary however is appropriate. You can create a new version using several versions of the constructor which access C-style char strings, C++ strings and a couple variations of formatted strings.
Once you have a CSString object created you can use it as you would any C++ style string however with the added benefit of several utility methods that allow you to split the string several ways, crop the string, manipulate case and several ways to do case-insensitive searches and compares.
The ucstring Class
The ucstring class is a variation of the standard C++ string class developed within NeL to support unicode strings. For this reason you will not find the ucstring class in any of the NeL namespaces. Not much explanation it necessary for this since if you know how to use std::string you will know how to use ucstring. There are three important additional methods provided on this class which merit explanation: toString(), fromUtf8() and toUtf8().
There are two variations of the toString() method: one which returns a std::string value and one which takes a std::string argument. The former you would use to convert a ucstring into a std::string for portions of your code which are not unicode-aware. The latter you would use if you need to convert a standard string into a unicode string for portions of code which are strictly unicode.
The fromUtf8() and toUtf8() are used for conversion between UTF8 and UCS2 strings.
Utility Functions and Classes
For people not using the CSString class there are also a number of functions in the NLMISC namespace for string manipulation. Below is a table of interesting functions for std::string and ucstring users.
- bool testWildCard (const char *strIn, const char *wildCard)
- Checks strIn using wildCard - a wild-card string. Case-sensitive.
testWildCard("hello world", "hello*");
- void splitString(const std::string &str, const std::string &separator, std::vector< std::string> &retList)
- Splits str into a vector (retList) of strings using a separator. Also splitUCString() accepts ucstring instead of std::string. Case-sensitive.
std::vector<std::string> foo; splitString("a:b:c", ":", &foo);
- bool strFindReplace(T &str, const T &strFind, const U &strReplace)
- For both std::string and ucstring find a sub-string strFind in str and replace instances of it with strReplace. Case-sensitive.
strFindReplace("badString", "bad", "good");
- std::string toUpper (const std::string &str)
- Also toLower(), same syntax. Converts the entire case of str.
std::string foo = toUpper("foBaR");
- std::string stringFromVector (const std::vector< uint8 > &v, bool limited=true)
- Converts a vector of bytes into a readable string. Unprintable characters are replaced with '?'.
std::vector<uint8> bar; // Hi bar.add(72); bar.add(105); std::string foo = stringFromVector(&bar); // foo == "Hi"
- std::string bytesToHumanReadable (const std::string &bytes)
- Converts a string representing bytes into a human readable string.
std::string foo = bytesToHumanReadable("105123"); // foo is now "102kb"
- std::string bytesToHumanReadable (uint32 bytes)
- Converts an integer into a human readable string representing bytes.
std::string foo = bytesToHumanReadable(105123); // foo is now "102kb"
- uint32 humanReadableToBytes (const std::string &str)
- Converts a string with a human readable byte representation into the byte amount.
uint32 bytes = humanReadableToBytes("102kb"); // bytes is now 105123
- std::string secondsToHumanReadable (uint32 time)
- Converts a number of seconds into a human readable string.
std::string readableTime = secondsToHumanReadable(3600); //readableTime is now "1h"
- uint32 fromHumanReadable (const std::string &str)
- Converts a human readable time or bytes into a number of seconds.
int bytes = fromHumanReadable("102kb"); // bytes is now 105123 int seconds = fromHumanReadable("1h"); // seconds is now 3600
- void explode (const std::string &src, const std::string &sep, std::vector< std::string > &res, bool skipEmpty=false)
- Splits a string src by a separator sep into a vector res. The skipEmpty parameter informs explode to not replace the vector if there are no matches.
const std::string myString = "a:b:c:d"; const std::string mySep = ":"; std::vector<std::string> myVec; explode(myString, mySep, &myVec);
Internationalization
There are three tools you can use in implementing internationalization in NeL-based applications. There's the CI18N class, the STRING_MANAGER and finally unicode string class ucstring which was covered in an earlier section. The ucstring class is useful for providing unicode strings but in and of itself performs no internationalization.
The CI18N Class
The CI18N class is probably the most useful internationalization tool provided by NeL. A quick introduction to this in the form of code will illustrate how easy it is to use this tool:
Internationalization Sample
fr.uxt
// This is the name of the language. LanguageName [Français] Hi [Bonjour] PresentI18N [%s est fière de vous présenter le système d'internationalisation de NeL] ExitStr [Appuyez sur <entrée> pour quitter]
en.uxt
// This is the name of this language. LanguageName [English] Hi [Hello] PresentI18N [%s is proud to present you NeL Internationalisation system] ExitStr [Press <return> to exit]
sample.cpp
int main (int argc, char **argv) { createDebug(); InfoLog->displayRawNL("Please, choose 'en', 'fr' or 'de' and press <return>"); std::string langName; std::getline(std::cin, langName); // load the language CI18N::load(langName); InfoLog->displayRawNL(CI18N::get("Hi").toString().c_str()); InfoLog->displayRawNL(CI18N::get("PresentI18N").toString().c_str(), "Nevrax"); InfoLog->displayRawNL(CI18N::get("ExitStr").toString().c_str()); getchar(); return EXIT_SUCCESS; }
As you can see above you only need to create a key-value pair for a string ID (in the example this is referenced in the get() method) and the internationalized string.
Internationalization File Format
The file format for the NeL internationalization system is pretty simple but is very powerful. It allows you to comment your files as well as providing you a powerful preprocessing system. These files should always be named languagecode.uxt where valid language codes are flexible but good examples are en, fr, and de as seen in the sample above.
Labels and and label texts are the core component and most important part of the file format. As soon in the above example and following examples a label is the first set of alphanumeric characters on the line. The label text is indicated by enclosing it following label with the [ and ] characters. Label text can have newlines in them, allowing the response to be several lines long. You can also indicate the use of things or explain the purpose behind the file using comments. Comments come in the form of simple C-style comments.
In addition to a useful comment system the file format also has a system of preprocessor definitions. These allow you some limited programmatic flexibility (through defines and if-statements) as well as tremendously useful include system. It is important to note that the if-statements adhere to scope and therefore can be nested. Listed below are the available preprocessor definitions. The preprocessor if-statements have increased importance when using the #optional statement as well as the load proxy (discussed in the section about the CI18N class) as these inject some dynamics into the loading of your internationalization strings.
The system disables preprocessing by default - you will need to implement an CI18N::ILoadProxy class and use the setLoadProxy() method so that you can calls readTextFile() with your own arguments. See the discussion below about load proxies.
| Preprocessor Command | Description and Use |
|---|---|
| #include filename |
Includes the file specified into the internationalization file at the point this command is specified. |
| #optional filename |
Includes the filed specified into the internationalization file at the point this command is specified, however the file inclusion is optional and does not error if the file is not found. |
| #define value |
Inserts value into a list of predefined values. |
| #ifdef value |
If value is defined (see #define) then start a new scope, otherwise bypass loading labels between this statement and the corresponding #endif. |
| #ifndef value |
If value is defined (see #define) then start a new scope, otherwise bypass loading labels between this statement and the corresponding #endif. |
| #endif | Ends an if-statement scope. |
Using the CI18N Class
The CI18N class is a relatively simple string manipulation class. In the example provided above we saw how simple it was to load a flat file, specify the current language code, and then begin getting internationalized strings. In a typical application you will first load a translation file using the load(languageCode) method and then use the get(labelName) method to retrieve translated strings. The get() method returns a ucstring and can contain printf style tokens (e.g. %s.) In the above example you saw how we used printf tokens to send the translated string and arguments to a variable argument method such as printf or nlinfo.
There are a couple handy helper methods that you may find yourself using. The most commonly used is the hasTranslation(labelName) method. This is a quick way of checking to see if a label is available before you attempt to use it, especially if the label's availability is governed by and #optional include or one of the various if-statement preprocessor definitions. One thing that is important to note if you are a long-time NeL user is that the getLanguageNames() method is no longer in the API. OpenNeL lifted the artificial limitation of language codes.
One of the last large features of this system is the "load proxy" interface. The internationalization system allows you to proxy string loading into a custom class that implements the CI18N::ILoadProxy interface. This is used to bypass the default call to the readTextFile() method. Using an implementation of this interface you can call the readTextFile() method how you see fit, process the file text before you load it, or load in custom text using the readTextBuffer() method. There are a few common reasons for doing this such as loading the file and then merging "newer" data from a server or overriding the parameters to readTextFile() so that it performs preprocessing.
The STRING_MANAGER Utility - TODO
ddd
File and Path Management
The NeL framework provides a wealth of tools for both local and virtual file system manipulation and creation. In this section we'll cover the utility singletons CPath and CFile, reading and writing files on the local filesystem and the how to create and access the two virtual filesystems: BNP (Big Files) and XML Pack.
Using Path and File Utilities
There are two core tools for managing paths and files: CPath and CFile. The CPath singleton is a designed to abstract file-system operations and searches on a high level, including logical virtual filesystems. The CFile singleton is more low-level and designed for direct, local operating system access.
The CFile Utility
The CFile utility is tremendously useful when you need lower level direct access to the local file systems. It is, for the most part, an informational class providing various methods to retrieve a filename (separated from the path,) a path (separated from the filename,) file size and permissions. But it also includes some tremendously useful methods for file system operations such as copyFile() and method for file comparison and so on. Realistically checking the CFile API will provide more information than can be provided here but we will cover in a code example a couple simple scenarios.
std::string srcFile = "/home/me/test1/testfile.txt"; std::string othFIle = "/home/me/test1/test2.txt"; std::string tmpStr; // Produces a string containing "testfile.txt" tmpStr = CFile::getFilename(srcFile); // Produces a string containing "/home/me/" tmpStr = CFile::getPath(srcFile); // Produces a string containing "txt" tmpStr = CFile::getFileExtension(srcFile); // This check only works for files on the local system, not files in BNP or XML Packs. if(!CFile::isDirectory(srcFile) && CFile::fileExists(srcFile)) { // Note: getFileSize and getFileCreationDate use CPath and will work on BNP and XML Packs. nlinfo("this file is %d bytes and was created on %d (seconds from epoch,) last modified on %d (seconds from epoch).", CFile::getFileSize(srcFile), CFile::getFileCreationDate(srcFile), CFIle::getFileModificationDate(srcFile)); // Also has the ability to manipulate local directories and files. // Make sure we can write to this file. CFile::setRWAccess(srcFile); // Create a new directory. Also createDirectoryTree creates several branches deep. CFile::createDirectory("/home/me/test2"); // Copy our srcFile to the new directory. CFile::copyFile("/home/me/test2", srcFile); // Delete the old srcFile CFile::deleteFile(srcFile); srcFile="/home/me/test2/testfile.txt"; // Move our other file. CFile::moveFile("/home/me/test2", othFile); // Finally delete the old directory. CFile::deleteDirectory("/home/me/test1"); }
The above example really covers the vast majority of uses of this utility. There are a couple advanced items that are important to note: temporary file creation and file callbacks.
The CFile utility provides a method called getTemporaryOutputFilename(src,dest). You provide the method with a source file name and it will update the argument dest with a unique filename: e.g. testfile.txt becomes testfile.txt.tmp. Subsequently calls to the method will then begin to produce testfile.txt.tmp.1 and testfile.txt.tmp.2 and so on.
The file callbacks are a useful way to monitor whether a file has changed. To harness this you need to create a function that accepts a string argument and then use the addFileChangeCallback method. Here is a quick example.
void fileChanged(const std::string &filename) { nlinfo("The following file has changed: %s", filename.c_str()); } int main (int argc, char **argv) { createDebug(); CFile::addFileChangeCallback("testfile.txt"); // .. do some logic, maybe change the file... // finally cleanup at exit: CFile::removeFileChangeCallback("testfile.txt"); }
The CPath Utility
This utility is the most commonly used utility in the NeL file toolbox. It creates a logical layer above the physical file system and any virtual file systems such as Big Files and XML Packs. The goal is to provide developers with as simple an interface as possible and still maintain a high level of transparency and usability. In the course of typical usage you will first add search paths, possibly add some file or file extension remappings, perform lookups and finally you may have a need to reset the whole CPath object.
Adding Search Paths
There are several methods at your disposal for adding search paths. You'll typically add a search path so that the system can find common datafiles, configuration files and so on. So it is important to add search paths early in the program execution. An important concept for search paths is the alternative directories options. These are directories that are added to the search order but pre-cached. This means that they will only be searched during lookups once all primary search paths have been exhausted.
File Name and Extension Remapping
dd
Performing Lookups
dd
Resetting the Object
dd
Input and Output
ddd
Creating and Using Big Files
The Big File or BNP system is a logical virtual filesystem that allows packagers to place several files into one physical file. Access the files packaged within this .bnp file is completely transparent to developers of NeL-based applications as long as the bnp file is in one of the search paths. The CPath system completely abstracts finding and accessing these files from the developer. Before you can access BNP packages you will first need to know how to create them. Next you'll need to know some slightly advanced usage in accessing the BNP packages.
Creating BNP Files
In order to create .bnp files you will first need a copy of the bnp_make executable. If you do not have this you can compile it as part of the optional NeL tools. This tool is very simple has has three main actions: pack (/p), unpack (/u) and list (/l). It also accepts two optional parameters -if and -ifnot that take a wildcard string and filter the files that will be copied into the BNP file. Take the following directory for example:
$ pwd /home/me $ ls -l bnptest/ total 100 -rw-r--r-- 1 me me 61745 2008-05-19 12:47 gnu.shape -rw-r--r-- 1 me me 28998 2008-05-19 12:47 sky.shape
Here we have a directory with two shape files in it that we want to package into a single BNP file. Here's the simple command to accomplish this.
$ bnp_make /p bnptest Treating directory: /home/me/bnptest adding /home/me/bnptest/gnu.shape adding /home/me/bnptest/sky.shape
Loading and Access BNP Files
Whenever you add a search path to CPath it will automatically discover any bnpfiles in that search path. Alternatively if you have specific files that are not in your search path you can manually add them using the addSearchBigFilemethod. You will rarely need to manually add these to your CPath search paths, but the method is provided for flexibility, in the instance that a file is downloaded by a patcher after execution, for example.
Accessing BNP files is no different than using CPath to find any normal file. Rarely will you use CPath::lookup to find a BNP file, however you will use it to find any file contained within a BNP file. If you used the .bnp that we created in the previous section then loading a file from a BNP file is as simple as the following example:
// The fourth (4th) argument is lookupInLocalDirectory, it is important that this is false. std::string filename = CPath::lookup("gnu.shape", true, true, false); CIFile shapeIn(filename);
You can see how simple it was to find a file and load it into a CIFile. While you will rarely load a shape using CIFile and shape loading will be covered in a later section this does show how easy it is to lookup a file from within a bnp and load it. There is the possibility that you have two files with the same name, although this should be avoided at all costs. If you need to troubleshoot one quick way to verify which file you have received is to check the path of the file returned by the CPath::lookup call:
std::string filename = CPath::lookup("gnu.shape", true, true, false); if(filename.find("bnptest.bnp@gnu.shape") != std::string::npos) { // we found the correct file name. nlinfo("filepath is: %s", filename); // this outputs: filepath is: bnptest.bnp@gnu.shape }
Creating and Using XML Packs
The XML Pack mechanism is useful for packaging several different XML files in a single packed XML file. They function within NeL similar to Big Files and are abstracted by the CPath system. In this section we'll cover how to create XML packs, how to load them and how to access and use them.
Creating xml_pack Files
In order to create xml_pack files you will first need a copy of the xml_packer executable. If you do not have this you can compile it as part of the optional NeL tools. This tool is very simple and only has three (3) command line options: -p (pack), -r (recurse) and -u (unpack). Assume you have the following contents in your current directory:
$ pwd /home/me/xmltest $ ls -l total 8 -rw-r--r-- 1 me me 25 2008-05-15 15:26 bar.xml -rw-r--r-- 1 me me 26 2008-05-15 15:26 foo.xml
To convert these two files into a single XML Pack you would simply run this from the above directory:
$ xml_pack -p
This creates a new xml_pack file called xmltest.xml_pack in your current directory using every file in the directory. The xml_packer tool will add every file in your directory except other .xml_pack files. If you have multiple sub-directories you can also have the xml_packer tool add the files in those directories as well by adding in the -r command line option. Finally you can extract any xml_pack files you have back into their directories using the -u or unpack option.
It is important to note that xml_packer is intended for XML files but will pack everything in it's path. The only exception is the following extensions: xml_pack, xml_pack_index, log, and bin. It also ignores the CVS directories. Not being careful about what you pack could create harmful bugs in the future.
Loading and Access XML Packs
Whenever you add a search path to CPath it will automatically discover any xml_pack files in that search path. Alternatively if you have specific files that are not in your search path you can manually add them using the addSearchXmlpackFile method. Useful scenarios of this are if you are transmitting XML Packs of containing dynamic or updated data you may want or need to use the method to one-off add the file.
Accessing XML Packs is no different than using CPath to find any normal file. Rarely will you use CPath::lookup to find an XML Pack, however you will use it to find an XML file contained within an XML Pack. If you used the XML Pack that we created in the previous section then loading an XML file from an XML Pack is as simple as the following example:
// The fourth (4th) argument is lookupInLocalDirectory, it is important that this is false. std::string filename = CPath::lookup("bar.xml", true, true, false); CIFile barXml(filename);
You can see how simple it was to find a file and load it into a CIFile. There is the possibility that you have two files with the same name, although this should be avoided at all costs. If you need to troubleshoot one quick way to verify which file you have received is to check the path of the file returned by the CPath::lookup call:
std::string filename = CPath::lookup("bar.xml", true, true, false); if(filename.find("xmltest.xml_pack@@bar.xml") != std::string::npos) { // we found the correct file name. nlinfo("filepath is: %s", filename); // this outputs: filepath is: xmltest.xml_pack@@bar.xml }
Time Tools
The NeL framework provides a number of tools for timing and profiling NeL-based applications and games. The most commonly used tool is the hierarchical timer which allows you to see overall execution of code as well as atomic breakdowns of scopes or sections of code within the hierarchy. There are many ways to use this and customize it to your specific needs. NeL also provides a much simpler tool called a stop watch, which meters the execution time between two points in code execution. Both of these options will be covered in this section.
Hierarchical Timer
NeL provides a handful of very useful timer tools for troubleshooting and profiling the performance of NeL-based applications. The most commonly used tool is the hierarchical timer. The hierarchical timer is comprised largely if the CHTimer class and a handful of utility macros: H_AUTO, H_AUTO_USE, H_AUTO_DECL, H_BEFORE and H_AFTER. The hierarchical timer gives you the ability to gather time statistics for an entire function or a block of code and provides some methods for managing the data the timer collects as well as reporting this data someplace useful (typically the log.)
In most scenarios you will use H_AUTO, H_BEFORE and H_AFTER so we'll describe how to use these macros first. H_AUTO is the most often used macro in this toolkit. One thing you'll also notice in the code is that the macros have a block of text. This block of text identifies the "scope" that we're benchmarking. This will be explained in more depth later on. Here is an example of these three macros in use:
void CFoo::usingHAuto()
{
H_AUTO( SAMPLE_usingHAuto );
CFooBar::getSingleton().calculateSomething();
}
void CFoo::usingHBeforeAfter()
{
// We'll do some unimportant code here.
int i;
i++;
// Now we'll specify the important part of code to benchmark.
H_BEFORE( SAMPLE_usingtHBeforeAfter );
CFooBar::getSingleton().calculateSomething();
H_AFTER( SAMPLE_usingHBeforeAfter );
}
So you can see that in the method 'usingHAuto' we simply used the H_AUTO macro. This creates an instance of the timer object within the scope of the method and when the method exits and the object is destroyed it will report back to the CHTimer class. This over simplifies the process but is essential enough to describe how to use the methods for timing and benchmarking. In the method 'usingHBeforeAfter' you can see that rather than relying on scope to determine the timing interval we specify the block of code we want to benchmark. This is handy if you have a specific block of code that is plaguing you and you don't want the other activities in that method or tree of methods to contaminate your timing benchmarks.
H_BEFORE/AFTER obviously have an application that H_AUTO cannot provide - benchmarking a specific block of code regardless of scope. But there's one large benefit that H_AUTO has over its before/after counterpart - you never have to remember to provide the H_AFTER macro. Take this scenario for example:
// HUGE MESS void CFoo::exampleOne() { // We'll do some unimportant code here. int i; i++; // Now we'll specify the important part of code to benchmark. H_BEFORE( SAMPLE_usingtHBeforeAfter ); CFooBar::getSingleton().calculateSomething(); if(foo == 1) return; H_AFTER( SAMPLE_usingHBeforeAfter ); } // Correct way to do this void CFoo::exampleOne() { // We'll do some unimportant code here. int i; i++; // Now we'll specify the important part of code to benchmark. H_BEFORE( SAMPLE_usingtHBeforeAfter ); CFooBar::getSingleton().calculateSomething(); if(foo == 1) { H_AFTER( SAMPLE_usingHBeforeAfter ); return; } H_AFTER( SAMPLE_usingHBeforeAfter ); }
You can see in the above example that you need to make sure to call H_AFTER before every single exit point in your method or else you could (and will) end up with a huge mess in your benchmarking.
Finally there's the H_AUTO_DECL and H_AUTO_USE macros. These very important macros are used when you want to lump multiple blocks of calls together without having to rely on the strategic placement of H_AUTO calls whenever these blocks of code are called. Here's a quick example of how these blocks of code can be lumped together:
H_AUTO_DECL( SAMPLE_allCFooCalls );
void CFoo::buildSomething()
{
H_AUTO_USE( SAMPLE_allCFooCalls );
CFooBar::getSingleton().calculateSomething();
}
void CFoo::calculateSomething()
{
H_AUTO_USE(SAMPLE_allCFooCalls );
CFooBar::getSingleton().calculateSomething();
}
These macros alone will not produce the benchmarking and timing results you require. Ultimately you will need to start using the CHTimer class. This powerful class provides you a variety of useful methods to manage this process. First you will need to know how to start, stop, reset and display the benchmarking and timing statistics. Use the following code as an example:
NLMISC::CHTimer::startBench(); // Now lets do some stuff. CFoo::buildSomething(); CFoo::calculateSomething(); Scene->render(); // Now we're done, lets finish up the benchmarking. NLMISC::CHTimer::stopBench(); // Finally output the results to the InfoLog NLMISC::CHTimer::displaySummary();
That is the essentials of using the hierarchical timer classes. Please refer to the API documentation for some more information on the CHTimer class. There are several methods on that class to display the benchmarking and timing data in meaningful ways. It is also important to note that these macros perform no logic when NeL is compiled in RELEASE mode.
Stop Watch
The stop watch class is used for performance measurement and statistics. It allows you to measure the time elapsed with controls like start, stop, pause, and resume. It gives you more flexibility to measure specific metrics at the loss of the automation and reporting abilities of the hierarchal timer. It's very handy for troubleshooting where the hierarchal timer is useful for profiling and overall performance monitoring. The stop watch controls are fairly basic, see this example for a common scenario:
void foo()
{
// create the stop watch.
NLMISC::CStopWatch stopWatch;
// start the timer.
stopWatch.start();
// ... process some code here.
// stop the timer.
stopWatch.stop();
// and output the results.
nlinfo("Time Elapsed in Milliseconds: %d", stopWatch.getDuration());
}
The stop watch system also gives you a handful of other methods to control how you accumulate time. Above we saw the use of start and stop and how to use the getDuration method to display the amount of time that elapsed between the start and stop call. Here we'll see how you can pause execution to skip a bit of code and resume accumulating time later. Also we'll see how you can artificially add time to the stop watch using the addTime method which can be useful if you're skipping a block of logic and want to assume that it took a predetermined amount of time. Finally you can start and stop a stop watch several times and output the average time that elapsed over each of the calls.
void foo()
{
// create the stop watch.
NLMISC::CStopWatch stopWatch;
// start the timer.
stopWatch.start();
// ... process some code here.
// pause the timer..
stopWatch.pause();
// ... process some code that shouldn't affect your dynamically.
// add the time we want to assume the above code took.
stopWatch.addTime( /* time here is in ticks... */ 1500 );
// resume the timer.
stopWatch.resume();
// ... process some code here.
// stop the timer.
stopWatch.stop();
// start the timer.
stopWatch.start();
// ... process the same code here.
// stop the timer.
stopWatch.stop();
// and output the results.
nlinfo("Time Elapsed in Milliseconds: %d", stopWatch.getDuration());
nlinfo("Average Time of Both start/stop calls in Milliseconds: %d", stopWatch.getAverageDuration());
}
In the above examples we saw two ways to display the stopwatch timing information: getDuration and getAverageDuration. The method getDuration will only provide you with time information in milliseconds for the period after the last call to stop. That means if you call start and stop several times - you will only see the last, most recent call to stop. The other handy display method, getAverageDuration, display the average elapsed time for all calls to start and stop.
Advanced
Events and Managing Input
Summary of Mananging INput and Events
Keyboard Input
todo
Mouse Input
todo
Game Device Input
todo
Streams
todo
Serialization
todo
Threading
The NLMISC module of NeL has a mixture of tools for programmers to make their applications threaded and to ensure thread-safety. This ranges from interfaces and classes to enable simple thread execution and monitor to controls to ensure thread-safety.
IRunnable Objects
The interface IRunnable is a basic interface that serves as the basis for most threaded logic. If you have any logic that you want executed you will create a sub-class of IRunnable. Here is the IRunnable interface:
class IRunnable
{
public:
// Called when a thread is run.
virtual void run()=0;
virtual ~IRunnable() { }
virtual void getName(std::string &result) const { result = "NoName"; }
};
You can see that the interface is fairly simple. All you're required to do is implement the run method with your logic. You don't have to implement the getName method but it is highly advisable that you do for troubleshooting. You will soon have logs filled with a thread name of "NoName" and a thread ID that outside of a debugger makes no sense. Below is an example of some logic created within IRunnable that can be threaded.
class HelloLoopThread : public IRunnable { void run() { while(true) printf("Hello World!\n"); } void getName(std::string &result) const { result = "HelloLoopThread"; } };
Thread Management
Simply having an instance of an IRunnable object does you no good - you must have some way to create a thread and manage that thread. This is where the IThread class comes into play. The IThread class provides an interface to store an instance of a thread that you will manage but also provides a couple useful static methods. We'll start with the static methods: create and getCurrentThread. Both of these static methods return an instance of an IThread object. The method getCurrentThread will provide you with the instance of the thread your code is currently executing in but the most important method is the static create method.
The create method is the method you use to convert IRunnable objects into threads. It takes an IRunnable object and a the stack size. If the stack size you provide is 0 - then it will use the default stack size as determined by the OS. Only provide a stack size if you have a specific reason for doing so - in nearly all scenarios providing only the runnable instance is sufficient.
Once you have created a thread object it isn't automatically running - you will have to manipulate it as you see fit which includes starting it. In addition to a method to start it the IThread class provides you with a few more essential methods for controlling the thread: start, terminate, wait and isRunning. The start method starts the thread, terminate will kill the thread before it reaches it's natural exit point, wait will force your current logic to block (wait) for the thread to complete and finally isRunning provides a bool return value to tell you if the object is currently running. Assuming that you have created the runnable from the previous example, here is an example of how you would manipulate the HelloLoopThread:
void threadManager()
{
IThread *helloThread = IThread::create(new HelloLoopThread());
helloThread->start();
nlSleep(100);
if(helloThread->isRunning()) // assume it's stick in an infinite loop.
helloThread->terminate();
}
Task Manager
Not all instances of IRunnable must be convverted into thread objects. Another alternative is the task manager system. You can see an example of this at work in the CAsyncFileManager class. This class is a perfect example of implementing performance critical components of a game in a thread-safe way.
Mutexes
When you create a mutex you will typically use the generic CMutex which is actually a CFairMutex. You do have a variety of mutex types available but in almost all instances you should be fine using the CMutex class. There are a couple important things to note about the use of any of the NeL mutex implementations. You must never attempt to share a mutex across processes. You also must not call enter in succession - each enter must be followed by a leave before calling enter again.and always enter and leave in the same order.
- CFairMutex (or CMutex) - this is a fair mutex.
- CUnfairMutex - this is a classic mutex and can be assumed to not be fair.
- CFastMutex - this is an unfair mutex with low-level optimizations for speed.
- CSharedMutex - this is a method of sharing a mutex between processes without explicitly sharing the handle. This is very advanced.
Using a mutex with NeL is very simple, to illustrate here's a brief example:
class CMyClass
{
// Create a mutex.
CMutex m_Mutex;
public:
void setValue(uint32 val)
{
// enter the mutex.
m_Mutex.enter();
// work with the values you want to be threadsafe.
m_MyVal = val;
// exit the mutex.
m_Mutex.leave();
}
};
In almost all scenarios you will use CMutex but in some special, specific cases you may use CFastMutex where speed is of the essence and you are not worried about thread-lock.
Synchronized Objects
So you can see above how you quickly used the low level mutex classes to make a block of code thread-safe. But there's a much easier and more reliable way of making a specific variable thread-safe. The synchronized objects templates are the most common way of implementing thread safety in NeL. By wrapping your variable in the provided templates you effectively force any access to the variable to be thread-safe. There are two main synchronized object templates:
- CFairSynchronized (CSynchronized) - this is uses a fair mutex to ensure thread-safety.
- CUnfairSynchronized - this uses an unfair mutex to ensure thread-safety.
Of the two templates the most frequently used one is CSynchronized. CUnfairSynchronized is only used in cases where performance is of a concern and thread-lock is not considered to be an issue. The following example shows how to use the CSynchronized template to protect a member:
class CMyClass
{
CSynchronized<uint32> m_Foo;
public:
// the member m_Foo will be thread-safe for the duration of the scope of this method.
void setFooValue(uint32 val)
{
// Get an accessor to the member.
CSynchronized<CFoo>::CAccessor access(m_Foo);
// Set the value.
access.value() = val;
}
};
Library Loading
NeL provides a set of classes and macros to ease the loading and initialization of library loading. This is a system that NeL refers to as "Pure NeL Libraries." The essence of this system is the CLibrary class and the INelLibrary interface. As a client developer (using a pure NeL library) you will use the CLibrary class extensively. As a plugin/module developer you will use the INelLibrary interface. Before you can use a plugin or module you must create it.
Pure NeL Library Creation
Your first step in creating a pure NeL library will be to create the library class that will be loaded. This class inherits from the interface INelLibrary and provides a handful of methods to the library loading mechanism. There are two important methods to be implemented on this class: onLibraryLoaded and onLibraryUnloaded. They both accept a bool for a parameter. This parameter (called firstTime and lastTime, respectively) specifies the order in which this class is loaded and unloaded. Finally there's an important macro that you will need to use called NLMISC_DECL_PURE_LIB - this does the hard work of exporting your library in a manner that the CLibrary class can access it. Here's an example of a pure NeL library:
class CMyLibrary : public INelLibrary { public: void onLibraryLoaded(bool firstTime) { // you definitely want to load up any data from the disk that this library will use when it is loaded // but you probably only want to do it once to save time. if(firstTime) loadDatafiles(); } void onLibraryUnloaded(bool lastTime) { // you would only want to delete/unload data if you knew this library was no longer in use. if(lastTime) unloadDatafiles(); } void loadDatafiles() { // this would load something from the disk. } void unloadDatafiles() { // maybe this would save out the data to the disk and then delete any allocated objects. } }; NLMISC_DECL_PURE_LIB(CMyLibrary);
The method onLibraryLoaded is called after each loading of the library. The pure NeL library system keeps track of how many places have requested this class to be loaded so that you can perform critical initial loading logic as well as any logic you may want to execute every time something requests this library. It is totally up to you as a plugin/library implementor. A common use of this is demonstrated in the sample above - the loading of any assets or allocation of any new objects necessary. There doesn't need to be any logic in this method, you could implement it as:
void onLibraryLoaded() {}
The method onLibraryUnloaded is called after each unloading/release of the library. The parameter "lastTime" will be true if this call is the last remaining reference to the library. You would commonly do things such as save out data, delete allocated objects and other cleanup things. Think of it as a library-level destructor. As with the onLibraryLoaded the system doesn't require you to put any logic in this method, simply implement it as shown above - with no logic.
Finally there's the NLMISC_DECL_PURE_LIB macro. You don't really need to know much about this other than that it must be issued in your implementation so that the CLibrary system can access the symbol to load your newly created INelLibrary implementation.
Pure NeL Library Loading
While you may never create a plugin/library using the INelLibrary interface explained above hopefully you will find need to load a plugin using this system. Loading a plugin using the CLibrary system is fairly easy. The system does provide you with a variety of useful utility methods as well. Before we get to the meat of loading a library we should talk about a couple of the utilities that are provided through the static public members of the CLibrary class
The two most important methods provided are makeLibName and addLibPath. The makeLibName method takes a library name (e.g. mymodule) and outputs a new string with a decorated NeL library name. This means that it will convert a library name into a NeL-compliant library filename. It decorates the name in accordance with the platform and compilation mode. On Windows in ReleaseDebug mode mymodule becomes "mymodule_rd.dll" and on Linux it becomes "libmymodule_rd.so." Because it is possible that you will load a library before you have had a chance to populate CPath with search paths the CLibrary method provides you with the addPath and addPaths methods so that you can provide it with information on how to find the dynamic library to load.
Now that we understand some of the basic utility functions we will create a new CLibrary handle and use the loadLibrary method to actually load a library. Before explaining how all of this works together an example might be helpful:
int main() { // Add the "modules" directory to the list of paths to search for dynamic libraries CLibrary::addPath("modules"); // Create a new library handle. CLibrary myModule; // Now load the library. mymodule. myModule.loadLibrary("mymodule", true, true, true) // Perform some logic here // ... // Now before we exit we should be good programmers and release the library. myModule.freeLibrary(); return 0; }
The loadLibrary method takes 4 parameters: the name of the library, whether to add NeL decoration, whether to use the library paths and finally whether this library handle "owns" the library being loaded.
bool loadLibrary(const std::string &libName, bool addNelDecoration, bool tryLibPath, bool ownership=true);
If you used makeLibName yourself then you would set the 2nd parameter to "false" so that it doesn't attempt to re-decorate your library name. These first two parameters are pretty self explanatory. The 3rd parameter tells the library handle whether to use the paths that you added using the addPath or addPaths methods. If this is true it will go through each path in the order added to the library handle. The final parameter, ownership, should always be true unless you are doing something very advanced. This specifies whether or not this handle owns the symbols that are loaded by this. You could potentially use this parameter to some advanced uses but it disables your ability to unload the library with this handle. This last point is very important - if you load a library specifying an ownership of false and then attempt to free the library your code will assert. Finally it will return true if it successfully loaded this library.
Internally the CLibrary system tracks whether or not the library that was loaded was a pure NeL library or not. While at first blush you may have assumed that this system was written specifically for pure NeL libraries it has the ability to dynamically load and unload non-NeL libraries but you lose a fair amount of functionality. It is still suggested to load any dynamic libraries within Ne