I started to wonder last night whether you could statically link C# code so I'd get just a single EXE. In my quest I came across ilMerge. I guess it's not "statically linking", but it does let you combine multiple assemblies into a single dll or exe.
So now, you can distribute a single binary even though you may be using a bunch of DLLs to build it. Pretty neat.
There are a couple of classes in most projects that end up being singletons. The two most adept for this paradigm are Configuration and Database Handle Pooling. Both get used by many objects, both should only be instantiated once for efficiency and data integrity and we don't want to have to pass either of them around all the time. Voila, perfect candidate for singleton's.
When i started working with ASP.NET, however I ran into a problem with this, since ASP.NET persists over many requests and is multithreaded. For configuration that's user specific, that could be a problem. For transactional Database handles it's definitely a problem. Simplest way is to put it into the HttpContext.Current.Items collection. But now your code is tied to ASP.NET. You could write wrappers that test the environment and wrap the objects in singleton or HttpContext as appropriate, but that still leaves the singleton being shared across threads, should you write other multi-threaded apps.
I finally found a solution (sorry, i can't give credit because it's been a while and I can't find the original article) to this in the form of a Thread Singleton. This pattern uses the System.Runtime.Remoting.Messaging.CallContext to store our singleton. I've used it so many times now, that i figured i'd better write it down here so i don't have to dig through old code every time i want to re-use it:
using System;
using System.Runtime.Remoting.Messaging;
namespace Claassen.Util
{
public class ThreadSingleton
{
/// <summary>
/// Some unique string that identifies this class.. I just use the
/// Namespace qualified name
/// </summary>
const string SINGLETON_ID = "Claassen.Util.ThreadSingleton";
/// <summary>
/// Accessor/Factory for the ThreadSingleton
/// </summary>
private static ThreadSingleton Current
{
get
{
// pull the object from the CallContext
ThreadSingleton threadSingleton = (ThreadSingleton)CallContext.GetData(SINGLETON_ID);
if( threadSingleton == null )
{
// if there was nothing in the CallContext, create and add it
threadSingleton = new ThreadSingleton();
CallContext.SetData(SINGLETON_ID,threadSingleton);
}
return threadSingleton;
}
}
// Class code would follow
}
}
Wrote a little program that launches LFS, clicks the appropriate buttons and sends strings to connect to a server defined by IP (it's called local in LFS for some reason), puts the player in the game and starts the game.
All works nicely, but man, is it ever an ugly hack. Hard-coding screen coordinates, putting in Thread.Sleep(n) so the UI can catch up, etc. But at least there is a way to automate the client.
Having run into a number of things I cannot accomplish with InSim in LFS, I've decided to venture down the ugly route of simulating the mouse. Not that horrible. InSim was never really designed for client automation.
I haven't tested this on LFS yet, just Calc.exe, but I finally found a way to do mouse clicks in another application from .NET.
The first route i went down was the SendMessage from user32.dll. Tried many different ways of getting the windowhandle and all that stuff, but no click ever materialized. Also found this method weird. Why do i have to tell it what window i'm clicking when i give it the cursor position. It's likely i just never figured out the proper syntax, but the docs i found certainly didn't aid me much.
Then i found mouse_event also from user32.dll. This one just sent a click event for the current location of the mouse. Just the way i would have expected. And it works!
Here's the sample code:
const int WM_LBUTTONDOWN = 0x0201;
const int WM_LBUTTONUP = 0x0202;
const int SC_MAXIMIZE = 0xF030;
const UInt32 MouseEventLeftDown = 0x0002;
const UInt32 MouseEventLeftUp = 0x0004;
[DllImport("user32.dll", EntryPoint="SendMessage", CharSet=CharSet.Auto)]
public static extern void SendMessage(IntPtr hWnd, int msg, int wParam, int lParam);
[DllImport("user32.dll")]
static extern void mouse_event(uint dwFlags, uint dx, uint dy, uint dwData, UIntPtr dwExtraInfo);
public static void Test()
{
Process myProc = new Process();
myProc.StartInfo.FileName = @"calc.exe";
myProc.Start();
// maximizing so we know where the button is (cheap ass hack for testing)
SendMessage(myProc.MainWindowHandle,WM_SYSCOMMAND,SC_MAXIMIZE,0);
Point p = new Point(150,150); // the 6 button
Cursor.Position = p;
Thread.Sleep(1000);
if (myProc.WaitForInputIdle(3000))
{
// This one works !!!
mouse_event(MouseEventLeftDown,0,0,0,new System.UIntPtr());
mouse_event(MouseEventLeftUp,0,0,0,new System.UIntPtr());
// This one doesn't do anything :(
SendMessage(myProc.MainWindowHandle,WM_LBUTTONDOWN, Cursor.Position.X, Cursor.Position.Y);
SendMessage(myProc.MainWindowHandle,WM_LBUTTONDOWN, Cursor.Position.X, Cursor.Position.Y);
}
}
I've been playing Racing Simulations for a while. I have an Act Labs Force RS, a wheel I truly enjoy, even though Act Labs has decided to get out of that market. My true love was Need For Speed: Porsche Unleashed. I think it was the best NFS game of the series. A good balance between Arcade and Simulator. However it is also the one that did the worst commercially and now NFS is pretty much a Console arcade racing franchise. Oh well.
I've spent some time with TOCA: Race Driver 2, a beautiful engine, but i never liked the feel of the cars. Grand Prix Legends, is of course the reigning champion among Simulation junkies, so much so that the 10 year(?) old game has been lovingly kept up to date by a devoted fanbase. But talk about hard to drive, and I am really more of a GT racing kind of guy.
While I've known about Live For Speed for a while and played the demo, it wasn't until a couple of weeks ago that I finally broke down and bought the full game. And man, it's shaping up to be my favorite simulator. S1 is already incredible, and graphically good enough. The driving is fun and challenging and extend to which you can tweak your car setup is incredible. And what's been posted of S2 pretty much puts at the top of the genre, if you ask me.
And then there is InSim, a UDP protocol for communicating with either the individual game or a game server to control many aspects, or syphon racing statistics from it. Being a geek, I was drawn to this protocol almost as much as to the racing itself and so I've been busy building an Object/Event model to encapsulate the protocol in C#. Finally all the playing with binary serialization has paid off. The first version of the lib with a VS.NET winforms tester solution can be found here, the full NDoc generate docs are here. Right now this is a binary release of the Lib itself, but once i lock down the Model, i'll release the full source, most likely under the General Public License.
This initial release understands all InSim packets, but I have not built objects around them, so really the only capabilities exposed are:
Connect to an LFS instance, and if so configured, automatically respond to keep alive packets
Access the LFS version information
Send and receive messages
Request and subscribe to Status packets.
It'll probably take me a month to flesh out the rest, at my current pace.
I've been spending some time building an Object Model around an binary wire protocol. It uses traditional C style strings which are null terminated. C# strings have no problem '\0', so i was getting some funny text messages, as well no way to see an empty string. I was hoping that there was something in BitConverter or Encoding that would truncate the byte array magically, but i wasn't able to find it. So, pending a a better solution, i convert the whole thing to a string, then use IndexOf() to find the first '\0' and then copy the substring up to the null into another string. Ugly, IMHO, but it works.
Came across an annoying bug with ListBox. If you use BeginUpdate() and EndUpdate() and only add a single item to a cleared Item collection, then that Item does not show up on Refresh. However if you leave the BeginUpdate()/EndUpdate() out, it works fine, resulting in a need for code like this:
listBox.Items.Clear();
if( itemList.Count > 1 )
{
//only use the Begin/End cycle if we have more than one item
itemList.BeginUpdate();
}
foreach( object item in itemList )
{
listBox.Items.Add(item);
}
if( group.Strings.Count > 1 )
{
//only use the Begin/End cycle if we have more than one item
mStrings.EndUpdate();
}
listBox.Refresh();
Just a note of something simple and silly enough that I shouldn't forget it again: Filepaths in log4net configurations still have to escape the DOS separator backslash. I know it should be obvious, you use \ in a text file, you escape it.
Well, i was too used to the @"c:\foo\bar.txt" syntax from C# that when i took the path out of the code and into the log4net XML config, i left it in the c:\foo\bar.txt format. I wasn't able to wrap a debugger around the deployed code and couldn't figure out why my logger wasn't writing. I figured out how to get to the currently available appenders at runtime and wrote that to the web page trace and seeing that there was no filename for the RollingFile appender, smacked me in the head with the "escape the backslash, stupid".
The silly part out of the way, this did give me some useful insight into log4net internals. So to get at the configured appenders you can do this:
// Just grabbing the first logger, since we want to use it to grab it's parent,
// the root logger
log4net.Repository.Hierarchy.Logger logger
= (log4net.Repository.Hierarchy.Logger)log.Logger.Repository.GetCurrentLoggers()[0];
Console.WriteLine("Logger: {0}",logger.Name);
//
foreach( log4net.Appender.IAppender appender in logger.Parent.Appenders )
{
Console.WriteLine(" Appender: {0}",appender.Name);
// just checking for RollingFile here, but could just as well check for all other appenders
if(appender.GetType() == typeof(log4net.Appender.RollingFileAppender))
{
log4net.Appender.RollingFileAppender rolling
= (log4net.Appender.RollingFileAppender)appender;
Console.WriteLine(" Rolling File: {0}",rolling.File);
}
Mind you this is just a place to get started from. It does make a number of assumptions you shouldn't make in production code. It is good enough though to help you debug log4net behavior.
A traditional way for overwriting a file atomically is to write the new file to temp file, and then use move to replace the original with the new. That way the replacement is atomic and no script that relies on the file existing and being complete will ever fail.
File.Move() in .NET is not the tool for this job. It'll throw an IOException because the destination exists, and there isn't an alternative way of calling it. You can delete the destination first, but then anyone relying on the file existing is screwed. Worse, someone might still try to re-create the file.
However, File.Copy(), does have an alternative overload that let's specify a bool to determine overwriting behavior. This way, the file will always exist, although someone could try to read it in an inconsistent state. And it's an ugly solution if the file is large. Grrrr...