Sunday, January 6, 2008

WoW Chat Project Part 2

So let's jump right in.  We've got the program starting and running fine, and setting the proper channel.  Now we have to handle the event that fires when the user presses "Enter".  We want to write the text of the entry window to the chat window, clear the entry window and set the channel back.

To get us started, we want to add an event handler to the Form1 constructor:

public Form1()
{
InitializeComponent();

rChatEntry.KeyDown += new KeyEventHandler(chatEnterPressed);
}

I covered event handlers in the SimConnect tutorials, but the whole handler/delegate concept is pretty simple.  When something happens in specified control (in this case, that's the chat entry window, rChatEntry) it fires off an event.  Since we've registered an event handler, the event has a delegate, which is simply a function that runs when the event gets fired.


So now we need chatEnterPressed.

private void chatEnterPressed(object sender, KeyEventArgs e)
{
// check to see if Enter was pressed
if (e.KeyValue == 13)
{
if (rChatEntry.Text.Length != lineStart)
{
}

e.Handled = true;
}
}

This is essentially just the frame, we'll get to the guts of the function in a second.


An event handler function like this has parameters for the sender which is simply whatever control or object sent the event.  The variable e represents whatever arguments got sent along with the event, in this case, it's info about the keypress.   For example, we are testing here for a specific key value, 13, which is the Enter button. 


Then we throw in a little validation to make sure the length of our total string (channel name + user input) is not the same as our lineStart value.  You may recall, lineStart is our "real" starting position, meaning how far along the chat entry text string where user input starts.  So if the values are the same, there's been no user input.  We don't want to send empty lines to the chat, so we have that check in there.


The last little bit is e.Handled = true.   When we get an event like a keypress, the event handler's delegate gets called, but there is still a default handling of the keypress.   In the case of the textbox like we are dealing with it's just a new line, but we don't want that.  We want to go back to the start of the first line.  So we say that the event is "Handled" so that any other event handlers don't touch it.


Now for the guts:

if (lineStart != rChatEntry.Text.Length)
{
// add the line to the chat window
rChatWindow.SelectedRtf = rChatEntry.Rtf;

// clear the text entry
rChatEntry.Select(0, rChatEntry.Text.Length);
rChatEntry.SelectionProtected = false;
rChatEntry.Text = System.String.Empty;

// Set the channel back
chatSetChannel(selectedChannel, selectedColor);

// Scroll to the proper point
rChatWindow.ScrollToCaret();
}

rChatWindow.SelectedRtf  is the spot in the chat window where our cursor/caret/pointer whatever you want to call it is.  We've used the Text property to get the contents of textboxs, but with Rich Text, we can copy the formatting over as well.


Then we select everything in the chat window, set it to unprotected, and empty the string out.  Now we should have a blank chat entry field, and the line of chat is in the chat window.  


Back to our handy helper function to set the Channel name in the entry box.  The last little bit is a textbox function that scrolls the window (if necessary) to the position of the caret.  In this case, what it means is that if the text in the window gets long enough to cause a vertical scroll bar to appear, the window scrolls down to show whatever new line is added.


autochannel4


Looks like that is working fine. 


Now for the real point of this all, the channel switching. 


The real simple way to assign an event handler for the TextChanged property is just going into the designer view and double clicking on our chat entry bar.


It should create the function for you.  I renamed the generated function to chatTextChanged to fit with everything else.  There are some good tools in here for renaming or refactoring.  By clicking on the little red underline that appears or by pressing Shift-Alt-F10, you get the rename dialog, and can select to rename the function.  This is important because there's an event handler auto-generated that assigns the new function as a delegate.  If we change the name of the function without updating the event handler through the rename dialog, we end up with errors and general not-working-ness.


This event will fire whenever there's a change to the text in the entry window.  We'll then check to see if the text is two characters longer than our lineStart value, for the "/g" or whatever we need.  If so, the next step is to select just those two characters, set it to lower case for matching purposes and match it to a list of possible solutions.  If it doesn't match any of them, just deselect it all.


The rest is simple, because we've already got a helper function to set the channel.


So here's our code to do it.

private void chatTextChanged(object sender, EventArgs e)
{
// test if the first two characters are /something. If there are more than two characters, skip it

if (rChatEntry.Text.Length == lineStart + 2)
{

// use a switch to test for our cases
rChatEntry.Select(rChatEntry.Text.Length - 2, rChatEntry.Text.Length);

switch (rChatEntry.SelectedText.ToLower())
{
case "/g":
// Switch to guild chat
chatSetChannel("Guild", Color.Green);
break;

case "/1":
// Switch to general chat
chatSetChannel("General", Color.Black);
break;

case "/2":
// Switch to trade channel
chatSetChannel("Trade", Color.Orange);
break;

case "/s":
// switch back to local "saying"
chatSetChannel("Say", Color.Red);
break;

default:
// deselect all
rChatEntry.Select(rChatEntry.Text.Length, rChatEntry.Text.Length);
break;
}
}
}

 


Walking through it, we have the test to check if we only have two user-inputted characters, selecting those two characters, and then a switch statement to test for the various slash commands we are looking for.  Notice in the switch line that we use the ToLower method so that "/G" comes out the same as "/g".


Then for each case, we set the channel using our helper program, and if it doesn't match anything, we deselect our selection.


autochannel5


Looks like it works fine.  I added in functions for the context menu items and a little mouse click handler that keeps the user from changing our position in the chat window.  Otherwise, it could be possible to insert chat lines in the middle of other lines. 


Here's the code as I have it:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;

namespace AutoChatChannels
{
public partial class Form1 : Form
{
// The currently selected channel
private string selectedChannel = "General";
private Color selectedColor = Color.Black;

// Since we are appending a channel name to the beginning of the line, we need to keep our own count
// where the "real" line (i.e. the user input) starts
private int lineStart = 0;

public Form1()
{
InitializeComponent();

rChatEntry.KeyDown += new KeyEventHandler(chatEnterPressed);
rChatWindow.MouseDown += new MouseEventHandler(rChatWindow_MouseClick);
}

void rChatWindow_MouseClick(object sender, MouseEventArgs e)
{

rChatWindow.Select(rChatWindow.Text.Length, rChatWindow.Text.Length);
}

private void Form1_Load(object sender, EventArgs e)
{
// Get datetime of entry into the channel
string currentDate = DateTime.Now.ToString();
// Write out the join message
string entryText = "You have joined the General Chat Channel at "+currentDate+System.Environment.NewLine;
// Add the text to the chat window
rChatWindow.Text += entryText;


// Now we have to select the text to make formatting changes to it.
rChatWindow.Select(lineStart, entryText.Length);

// make the text blue and bold
rChatWindow.SelectionColor = Color.Blue;

// Now make sure nothing is selected
rChatWindow.Select(rChatWindow.Text.Length, rChatWindow.Text.Length);

// Enter that in our text entry box
chatSetChannel(selectedChannel, selectedColor);
}

private void chatTextChanged(object sender, EventArgs e)
{
// test if the first two characters are /something. If there are more than two characters, skip it

if (rChatEntry.Text.Length == lineStart + 2)
{

// use a switch to test for our cases
rChatEntry.Select(rChatEntry.Text.Length - 2, rChatEntry.Text.Length);

switch (rChatEntry.SelectedText.ToLower())
{
case "/g":
// Switch to guild chat
chatSetChannel("Guild", Color.Green);
break;

case "/1":
// Switch to general chat
chatSetChannel("General", Color.Black);
break;

case "/2":
// Switch to trade channel
chatSetChannel("Trade", Color.Orange);
break;

case "/s":
// switch back to local "saying"
chatSetChannel("Say", Color.Red);
break;

default:
// deselect all
rChatEntry.Select(rChatEntry.Text.Length, rChatEntry.Text.Length);
break;
}
}
}

private void chatSetChannel(string channelName, Color channelColor)
{
// Select everything
rChatEntry.Select(0, rChatEntry.Text.Length);

// Unprotect everything first
rChatEntry.SelectionProtected = false;

// Set channel name
rChatEntry.Text = "<" + channelName + "> ";

// Select the text again
rChatEntry.Select(0, rChatEntry.Text.Length);

// Set the color
rChatEntry.SelectionColor = channelColor;

// Set the current text as protected
rChatEntry.SelectionProtected = true;

// Clear the selection to the end of the channel name
rChatEntry.Select(rChatEntry.Text.Length, rChatEntry.Text.Length);

// set our "real" start of line position
lineStart = rChatEntry.Text.Length;

// set our color
rChatEntry.ForeColor = channelColor;

// Selected Channel/Color
selectedChannel = channelName;
selectedColor = channelColor;
}

private void chatEnterPressed(object sender, KeyEventArgs e)
{
// check to see if Enter was pressed
if (e.KeyValue == 13)
{
if (lineStart != rChatEntry.Text.Length)
{
// add the line to the chat window
rChatWindow.SelectedRtf = rChatEntry.Rtf;

// clear the text entry
rChatEntry.Select(0, rChatEntry.Text.Length);
rChatEntry.SelectionProtected = false;
rChatEntry.Text = System.String.Empty;

// Set the channel back
chatSetChannel(selectedChannel, selectedColor);

// Scroll to the proper point
rChatWindow.ScrollToCaret();
}

e.Handled = true;
}
}

private void menuitemGeneralChannel_Click(object sender, EventArgs e)
{
chatSetChannel("General", Color.Black);
}

private void menuitemTradeChannel_Click(object sender, EventArgs e)
{
chatSetChannel("Trade", Color.Orange);
}

private void menuitemGuildChannel_Click(object sender, EventArgs e)
{
chatSetChannel("Guild", Color.Green);
}
}
}

 


Pretty simple little process all in all, I'd like to build off of it down the road, maybe incorporate it into something.


Please feel free to drop any questions in there for me.

Labels: , ,

Saturday, January 5, 2008

WoWing a Small Chat Project

One of the goals of the new programming focus this year was to try a variety of small projects,  small exercises to stimulate the brain and increase my programming agility.  There's no shortage of things to try, so I consider this a warm-up of sorts.

If any of you have played World of Warcraft, you'll get the idea pretty quickly, for everyone else, in WoW, there is a chat box that allows you to talk to everyone else in the game world.  To make this as efficient as possible (theoretically), the chat is split into channels.  You can use a menu to select what channel you are talking to, or just type "slash commands" which is a forward slash (/) followed by a letter or number to indicate the channel you want.  When you do this, the slash command gets replaced with the name of the channel.   The goal of this project is to emulate that.

I figure that this project presents a few challenges.  First off, finding the most effective way of detecting the entry of a slash command.  Once we get the channel name in there, it's part of the text string, so we want to have a way of pushing what we consider the "start" of the entered text.  Once we get lines of chat added, we want the chat window to keep our place when new lines are added.  So we've got a few things to tackle.

Stage One: Planning -

The way I figure, the best way to go about this is to have a pair of richTextBoxes (which can do the necessary formatting easily), one for the chat entry, one for the chat window displaying the already entered lines of text.  There would also be a menu detached from the top of the window that will be the alternate method of changing channels.

Here's the naming conventions I use:

Chat Entry window - rChatEntry
Chat display window - rChatWindow
Channel Selection window - menuChannelSelector

And here's what it looks like in designer:

autochannel1

Not the most complicated design ever.  The "Channel --->" part is the Menu, don't feel that you need to add it yet.

A few things to note:

- both text boxes are multiline = true, the bottom window (rChatWindow) is read-only and scrollbars = vertical.   rChatEntry has it's tab index set to 0 (zero) so that's the first thing that gets input focus on loading.
- I toyed with disabling rChatWindow completely, but learned quickly that then you couldn't scroll back up.  This ended up presenting it's own issues later on.
- I'm using Visual Studio 2008 Professional on Vista Home Premium, Aero turned off for that little bit of extra juice, just in case your view looks different.  Oh, and big one, I believe this is built for .NET 3.5.

I'll get to the context menu later.

Continuing the planning, let's lay out the structure of what's going to be happening here:

We're basically working completely off of events here, so we have a few functions to handle these events:
- chatTextChanged,  called whenever a change is made to the text of rChatEntry.  Previously, this could be handled by detecting a keypress, but this is the preferred way of going about it.  This function will see if we've entered a slash command and handle it properly.
- chatEnterPressed, called whenever you hit the enter key on your keyboard.  While our chat entry line will wrap if you go past the end of the line, we want "enter" keypresses to send the line to the chat window.

In addition to this, we'll have some initial stuff happening on the loading of Form1 (I left the default name there) and a helper function called chatSetChannel that does the repetitive work of changing the channel.

 

So, let's get started.

public Form1()
{
InitializeComponent();

}

In our Form1 constructor, we've got some stuff to add later, but for now we are fine.


Right above that, let's get some of the needed variables in there:

// The currently selected channel
private string selectedChannel = "General";
private Color selectedColor = Color.Black;

These are to keep track of what channel we are on and what color is associated with it.  Thanks to .NET, we don't have to do much with the colors, there are plenty predefined.


Now, when the form loads, we want to put a message in the chat window that we've joined the General channel at this date and time, and get our entry box set up on the correct channel.  I like doing this in the OnLoad event, because it keeps it clean and separated from the constructor and it also makes sure everything is actually set and loaded before I go screwing around with it.  I don't really think that would be a problem, but I do it anyways.

private void Form1_Load(object sender, EventArgs e)
{
// Get datetime of entry into the channel
string currentDate = DateTime.Now.ToString();
// Write out the join message
string entryText = "You have joined the General Chat Channel at "+currentDate+System.Environment.NewLine;
// Add the text to the chat window
rChatWindow.Text += entryText;
}

In this first part, we're using .NET to get the current time, slapping it in a string and putting it in the chat window.  Piece of cake.  Run the code to give it a try and we should see this:


autochannel2


Outstanding.  There is, however, a reason why we used richTextBoxes instead of just normal textboxes, and that's formatting.  Programmatically, it works almost the same as it does through a UI.  You select the text you want to edit and apply the formatting to that selection.


In our case, we want to select the text and make it blue.


Right after that last bit of code:

// Now we have to select the text to make formatting changes to it.
rChatWindow.Select(0, entryText.Length);

// make the text blue and bold
rChatWindow.SelectionColor = Color.Blue;

The Select method on a richTextBox sets the range of text to be selected, in this case, from 0 (the beginning) to the end of the string.  Then we simply set the color of the selection with SelectionColor.  As I said earlier, the .NET framework helps with the colors.


If we run it now, we should probably see blue text.  What's interesting is that you could change a few things so that your input focus starts in rChatWindow and you'd see that at this point, the text would actually be highlighted in the box, as if you had selected it with a mouse.  We want to take care of that, and we do so with this:

// Now make sure nothing is selected
rChatWindow.Select(rChatWindow.Text.Length, rChatWindow.Text.Length);

The syntax is "Select (StartIndex, EndIndex)"  so why not make it (0,0)?  Well, the select function is more like a mouse than we even saw before, it places the caret, or flashing cursor, our position in the textbox at the point at the end of the selection.  So by using the Length property of the textbox, we are setting the cursor to the end, so we can append more lines on to the chat window without trouble.


The other thing we want to do is set our chat entry box to represent the current channel, which starts on
"General".  It's here where we get to explore some of the issues presented to us.  Turns out, most of them are easy to solve.  A quick rundown:  The textbox shows the name of the active channel in front of whatever you type, i.e. "<General> Here is your message".  We have to insert that channel tag and make sure the user can't alter it.  In addition to that, if the user types "/g" or another slash command as the first characters in the box, we need to change that "<General>" to the new active channel and color. 


To solve the issue, we need to know what the "real" starting point of the user inputted string is.  We're going to be sticking our own text in the beginning of the string, so we need a count of how much we've added.  This will give us an index pointing to where the user's entry began.  To do this, we're going back to the top where we've declared our variables and we're going to add an integer called lineStart.   This should reasonably tell us where to start our selections when searching user input.

// The currently selected channel
private string selectedChannel = "General";
private Color selectedColor = Color.Black;

// Since we are appending a channel name to the beginning of the line, we need to keep our own count
// where the "real" line (i.e. the user input) starts
private int lineStart = 0;

We initialize it to zero.  Now, for the sake of completeness, we could go back into our Form1_Load and replace the "0" in our select call with lineStart.


Now that we've knocked that out, we start building our chatSetChannel helper function.  We're going to be changing the channel name a few times in here, so we don't want to repeat the same code.

private void chatSetChannel(string channelName, Color channelColor)
{
// Select everything
rChatEntry.Select(0, rChatEntry.Text.Length);

// Unprotect everything first
rChatEntry.SelectionProtected = false;
}

These first two methods get us started with some validation.  Before anything else, we select the whole text box and make it unprotected.  Protected text can't be altered.  This is one of the benefits of richTextBoxes, and it's how we plan on keeping the user from messing with our channel name.


The rest of this is pretty straightforward.

// Set channel name
rChatEntry.Text = "<" + channelName + "> ";

// Select the text again
rChatEntry.Select(0, rChatEntry.Text.Length);

// Set the color
rChatEntry.SelectionColor = channelColor;

// Set the current text as protected
rChatEntry.SelectionProtected = true;

// Clear the selection to the end of the channel name
rChatEntry.Select(rChatEntry.Text.Length, rChatEntry.Text.Length);

We format the channel name in brackets and slap it in the text box.  Now that our text is longer, we select the whole thing again, and use the SelectionColor property to make it all whatever the channel color is.  Not very exciting, but the General channel is black, as we set at the beginning.  With this section selected, we protect it, locking it from changes.  This will protect anything entered in after the selection until we make a new selection, so we move our selection to the end, ready for user input.

// set our "real" start of line position
lineStart = rChatEntry.Text.Length;

// set our color
rChatEntry.ForeColor = channelColor;

// Selected Channel/Color
selectedChannel = channelName;
selectedColor = channelColor;

Now, some housekeeping, we update the lineStart variable to indicate where the user input begins in rChatEntry and make the foreground (which means the text, in this case) color to our channel color.  This is so whatever the user types is the same color as the channel name.  Wrapping it up, we set our channel tracking variables to the values of the current channel.  That doesn't change right now, but it will when we are actually switching channels.


 


All of which will come tomorrow as it is very late right now.    Here's the code so far:


 

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;

namespace AutoChatChannels
{
public partial class Form1 : Form
{
// The currently selected channel
private string selectedChannel = "General";
private Color selectedColor = Color.Black;

// Since we are appending a channel name to the beginning of the line, we need to keep our own count
// where the "real" line (i.e. the user input) starts
private int lineStart = 0;

public Form1()
{
InitializeComponent();
}
        private void Form1_Load(object sender, EventArgs e)
{
// Get datetime of entry into the channel
string currentDate = DateTime.Now.ToString();
// Write out the join message
string entryText = "You have joined the General Chat Channel at "+currentDate+System.Environment.NewLine;
// Add the text to the chat window
rChatWindow.Text += entryText;


// Now we have to select the text to make formatting changes to it.
rChatWindow.Select(lineStart, entryText.Length);

// make the text blue and bold
rChatWindow.SelectionColor = Color.Blue;

// Now make sure nothing is selected
rChatWindow.Select(rChatWindow.Text.Length, rChatWindow.Text.Length);

// Enter that in our text entry box
chatSetChannel(selectedChannel, selectedColor);
}

private void chatSetChannel(string channelName, Color channelColor)
{
// Select everything
rChatEntry.Select(0, rChatEntry.Text.Length);

// Unprotect everything first
rChatEntry.SelectionProtected = false;

// Set channel name
rChatEntry.Text = "<" + channelName + "> ";

// Select the text again
rChatEntry.Select(0, rChatEntry.Text.Length);

// Set the color
rChatEntry.SelectionColor = channelColor;

// Set the current text as protected
rChatEntry.SelectionProtected = true;

// Clear the selection to the end of the channel name
rChatEntry.Select(rChatEntry.Text.Length, rChatEntry.Text.Length);

// set our "real" start of line position
lineStart = rChatEntry.Text.Length;

// set our color
rChatEntry.ForeColor = channelColor;

// Selected Channel/Color
selectedChannel = channelName;
selectedColor = channelColor;
}
}
}

When I give it a shot, I get this:


autochannel3


Next I'll be making chat entry actually enter chat into the chat window and allow for the changing of colors.

Labels: ,

Wednesday, January 2, 2008

Designer Jeans?

I was challenged today to make a post about designer jeans, and relate it to programming, which is somewhat daunting.

I ventured into the SB forums and was a bit surprised to see the posts I found there. I guess everyone is not as aware of the absolute volume of work put into this program and its ancillaries, a project reaching almost 100,000 lines of code, and maintained by one man. Daunting to take over, daunting to step into the light of expectation from the current userbase of SB3.

Of course, Joel has done most of the work. As he's mentioned before, the program is, for the most part, done. Portions have been in testing with a beta team, and a good amount of the core development has been checked off.

So what's the delay? Well, for starters, what has been said on the forum is true. I am coming in dark to this all. There is an awful lot to learn, and I've been taking it all in as fast as possible. I am in no way starting over, and the project is not getting abandoned. For those of you who don't use SB, there are several "code trees" at work here. There are internal (module) and external (exe) versions, both of these working on either FSUIPC or SimConnect, and the FSUIPC versions having elements for FS8 and FS9, which continue to be supported. Most of this is done. Let me clarify ahead of time what I mean by "done", and that is: the program elements are working in test situations. This is not the same done as in "If it's done, why not package up what you have and release it while you keep working on new stuff?" done. It's not there, and I don't think Joel or I would want to put that sort of statement of quality on our work anyways.

So what's left is that I'm now writing the module portion for FSX, and progressing along nicely. Once that is done, which includes loading the module at launch, establishing a connection through SimConnect, activating the menu item in the sim and handling the loading of the internal program window when you use said menu item, then there is the matter of testing all of it, which I believe Joel already has a good mechanism in place for, as well as going through the whole codebase and updating everything to the latest version specs, cleaning stuff up, some more testing, some more cleaning, packaging it up and getting it on its merry way.

Really, I would have loved to have had something for Christmas or New Years, but I had to selfishly go and get engaged.

But Joel and I have talked a lot about this and I'm in it for the long-haul. I'm very excited to see where SB and Flight Sim go after this version.

So much for combining this with designer jeans.

Labels: , ,

Thursday, December 7, 2006

A Ten-Thousand Foot View of SimConnect - Part 3

By this point, we have a basic operation that connects to SimConnect at the click of a button...and does nothing. So it's about time we start looking at the framework of how to actually interact with FSX.

You may have read, heard or gotten involved in the conversations about multithreading, hyperthreading, fibers, etc., but all you really need to know about the whole concept is the message queue. As we said before, your SimConnect program (like other Windows applications) is Event Driven. This means that the program acts on events sent to it from the system. Say for instance, you click on our "Connect" button. The operating system registers that mouse click, and sends a message to the program. The program gets the message, containing the click event, and fires off our function in response. A lot of this is transparent, because it is built into Windows and the .NET framework, but we have the ability to expose some of the functionality and expand upon it, which is exactly what we plan to do. In SimConnect, events and data are sent to our program via system messages.

For an in-depth look at the Windows System Message system, check out this page:
Messages and Message Queues

The core of our application's message system is the "Windows Procedures" or WinProc. It is a function that receives system messages and figures out what to do with them. We want to have our program handle the SimConnect messages when they come down the pipe, so we use an ability in programming to "override" the default function. This just means that we are taking an already defined function and creating our own version of it. In the case of the WinProc, it will look something like this:

protected override void DefWndProc(ref Message m)
{
  if (m.Msg == WM_USER_SIMCONNECT)
  {
    if (simconnect != null)
    {
      simconnect.ReceiveMessage();
    }
  }
  else
  {
    base.DefWndProc(ref m);
  }
}

As you can see, we are defining the function called DefWndProc, or Default Windows Procedures. "Protected" refers to the accessibility of the function in a way, meaning who can access or alter it. We have already talked about override, that tells us that we are making our own version of an existing function, and void is the return type. Functions can return a value, such as if you have a function that adds two integers together, the return value could be the sum. When you see void, it means that there is no return value.


Looking closer at the meat of the function, we discover our old friend WM_USER_SIMCONNECT. You will recall that we defined this constant variable back at the beginning and sent him along with our connection function. This variable identifies the system messages coming from FSX. When we connect to SimConnect, we pass along this identifier, and whenever FSX sends a message back to us, it has that same value attached to it. So by looking for that value, we know that it has come from SimConnect. Then, the function continues: "if the simconnect object is not null (meaning we have connected to FSX already), then run the method simconnect.ReceiveMessage( )". This is the function within the SimConnect framework that processes the messages sent to us and figures out what we want to do with them. If it's not a message from FSX, process it as normal. Basically, there's no reason why we would ever need to change this. We can just stick it in our Form1 class under the button click function and know that it will do what we want.


So now we are ready to handle messages from SimConnect. The first one we want to look for is the message telling us that our add-on has successfully connected to FSX. The messages contain data structures, which are just variables grouped together into a consolidated object. Think of them like classes, but without the functions. All we need to know about them is that they are little bundles of data wrapped into one object. The SIMCONNECT_RECV_OPEN data structure contains a whole slew of information about the connection, the version of SimConnect, all of that lovely nonsense. Right now, we don't need any of that, we just need to know what happens, and when.


Luckily, ACES likes us, and has put that functionality into the SimConnect wrapper. After we have our connection to SimConnect, and therefore, our simconnect object is defined, we can start adding stuff to it. The first thing is event handlers. This doesn't require a whole lot of explanation. It's just a statement of what function to run when a certain event is received.


NOTE: You may have noticed already that Visual C# Express has an "autocomplete" function. This is incredibly useful (although sometimes it gets in your way when you are coding quickly), in that it automates certain procedures based on what you are probably trying to do. In the case of these event handlers, they will allow you to press the tab key to enter the default values, and will even create the new function for you, like what happened with our button click function.


Speaking of our button click function, here it is as it stands right now:


        private void button_Connect_Click(object sender, EventArgs e)
        {
            try
            {
                simconnect = new SimConnect("SimConnect Tutorial", this.Handle, WM_USER_SIMCONNECT, null, 0);
                textBox_Connect.Text = "Connected to FSX";
            }
            catch (COMException ex)
            {
                textBox_Connect.Text = "Unable to Connect";
            }
        }

Right after our the line that sets the textbox text to "Connected to FSX" but before the } (curly bracket), we are going to enter this line:

simconnect.OnRecvOpen += new SimConnect.RecvOpenEventHandler(simconnect_OnRecvOpen);



As soon as you type "simconnect.", a drop down box will appear with all of the properties and methods of this object, as I mentioned above. You can type in "OnRecvOpen" or find it on the list. So far, we've only used the equal sign by itself (=), but here we use plus sign equal sign (+=). This adds a value to a definition, as opposed to just assigning a value. In this case, it is adding something new to the function that processes when the "Open" message is received (which happens when SimConnect is connected). Immediately after typing "+=", you should be presented with an option to press tab to enter the default value, and then to press tab again to create the new function automagically. That's the method I followed here. As you can see, we are adding a new event handler to the OnRecvOpen event, that fires off the function "simconnect_OnRecvOpen". In this function, we define what we want to do when FSX has told us that we are connected.


Everyone with me still? Just in case more clarification is needed, I mapped it out in another of the "Worst Flowcharts Ever" (click for full-size)



As you can see, SimConnect sends messages (events) into the message queue, which the OS sends to our program. The DefWndProc passes them along to the simconnect object, which sees that there is an event handler for this event. On top of that, we have added our own event handler to the mix, and in that we do whatever we need to do.


If you tabbed through the creation of our event handler line, you will notice that it added the function "simconnect_OnRecvOpen" to the code. There's an exception in there right now, telling us that we haven't done anything with it yet. We want to remove that. In it's place, just cut and paste the following line from our button click function:

textBox_Connect.Text = "Connected to FSX";


Because, really, we don't really want to tell the user that we are connected until we are sure that we are connected. This waits until we receive notification from SimConnect that the connection is made, then changes the status. Make sure you remove that line from it's original position as well. If you want to be fancy, you can leave it in, but change the text to "Connecting..." or something like that. In most cases, it won't be up very long anyways.


We're going to add another event handler right under our first one. This time, it's to catch the event that SimConnect sends out if the user quits out of FSX. The handler is OnRecvQuit, and the line looks like this:

simconnect.OnRecvQuit += new SimConnect.RecvQuitEventHandler(simconnect_OnRecvQuit);


If we use tab and autocomplete again, it will create another function called simconnect_OnRecvQuit. We're just going to add a line in there to tell the user that we have been disconnected.


        void simconnect_OnRecvQuit(SimConnect sender, SIMCONNECT_RECV data)
        {
            textBox_Connect.Text = "Disconnected";
        }


Now, run the program (once again, click the green arrow or use the menu option Debug -> Start Debugging. The shortcut key is F5) without FSX running. If we press the connect button, we should get a message telling us "Unable to Connect". Load FSX, start a flight, and press connect again. Now it should report that we are connected. Leave the program running, and exit out of FSX. Now we are informed that SimConnect has disconnected.


And then we get an exception.


Go back up to the debug menu and select "Stop Debugging". VC# will take you back to the basic code view. What went wrong? Well, we don't have SimConnect running anymore, but we still have the simconnect object waiting for messages. So we need to get rid of the simconnect object within our program. Luckily, again, they give us a simple way to do this, called the Dispose method. Add these two lines of code to dispose of the simconnect object and set it back to null where we started:


        void simconnect_OnRecvQuit(SimConnect sender, SIMCONNECT_RECV data)
        {
            textBox_Connect.Text = "Disconnected";
            simconnect.Dispose();
            simconnect = null;
        }


Try it again and everything should work as expected.


Now we have a good framework for the program, have the connection and some events running, but we also want to think about what the user might want to do. For our last step tonight, we are going to expand our button click function to turn the connect button into a disconnect button when we connect.


This is going to take a few lines of code throughout our program. First, in our simconnect_OnRecvOpen function, we want to change the text of the button to "Disconnect". When the program connects to FSX, this will just change what the button says. Right after the textbox line, add the following line of code:

button_Connect.Text = "Disconnect";


Same deal with the simconnect_OnRecvQuit function:

button_Connect.Text = "Connect";


So now, when the program connects or disconnects, the button changes. Now to implement the functionality, we use a if - then - else loop. This is called a "conditional". If this is true, do this, else do this. I'll paste the new button click code and then explain it.


        private void button_Connect_Click(object sender, EventArgs e)
        {
            if (simconnect == null)
            {
                try
                {
                    simconnect = new SimConnect("SimConnect Tutorial", this.Handle, WM_USER_SIMCONNECT, null, 0);
                    textBox_Connect.Text = "Connecting...";
 
                    simconnect.OnRecvOpen += new SimConnect.RecvOpenEventHandler(simconnect_OnRecvOpen);
                    simconnect.OnRecvQuit += new SimConnect.RecvQuitEventHandler(simconnect_OnRecvQuit);
                }
                catch (COMException ex)
                {
                    textBox_Connect.Text = "Unable to Connect";
                }
            }
            else
            {
                textBox_Connect.Text = "Disconnected";
                button_Connect.Text = "Connect";
                simconnect.Dispose();
                simconnect = null;
            }
        }


If our simconnect object equals (== compares two values, as opposed to = which just sets a value) null, meaning that we do not yet have a connected simconnect object, then run through our connection procedure. ELSE, if it does not equals null, meaning that we are connected, then do the same thing as if FSX just quit. Set our text to show that there is no connection and dispose of the SimConnect object.


Try it out. Run your program and FSX and you should find yourself able to connect and disconnect as many times as you want, both by using the button and by closing FSX.


By now, hopefully a lot of the theory behind this is understood. I've tried to put a lot of detail into a very small bit of actual operation so that those who aren't familiar with the procedures of programming or C# or even just SimConnect will be able to ride along smoothly down the line.


As of now, we have a program that is able to connect and disconnect from SimConnect, can detect and handle events sent from FSX, and has the beginnings of a useable interface. Not bad for a few lessons, I think. Now that we have a framework for events, we are going to use that ability to send and listen to events, so that we can control the toggling of landing gear from outside the program, and then to detect when the gear are up or down.


Could this happen tomorrow? Maybe. I'll certainly try my best to get it done by the end of the day (Friday).


And finally, here is the full code as I have it right now for Form1.cs:


using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using Microsoft.FlightSimulator.SimConnect;
using System.Runtime.InteropServices;
 
namespace SimConnect_Tutorial
{
    public partial class Form1 : Form
    {
        SimConnect simconnect;
        const int WM_USER_SIMCONNECT = 0x0402;
 
        public Form1()
        { 
            InitializeComponent();
        }
 
        private void button_Connect_Click(object sender, EventArgs e)
        {
            if (simconnect == null)
            {
                try
                {
                    simconnect = new SimConnect("SimConnect Tutorial", this.Handle, WM_USER_SIMCONNECT, null, 0);
                    textBox_Connect.Text = "Connecting...";
 
                    simconnect.OnRecvOpen += new SimConnect.RecvOpenEventHandler(simconnect_OnRecvOpen);
                    simconnect.OnRecvQuit += new SimConnect.RecvQuitEventHandler(simconnect_OnRecvQuit);
                }
                catch (COMException ex)
                {
                    textBox_Connect.Text = "Unable to Connect";
                }
            }
            else
            {
                textBox_Connect.Text = "Disconnected";
                button_Connect.Text = "Connect";
                simconnect.Dispose();
                simconnect = null;
            }
        }
 
        void simconnect_OnRecvQuit(SimConnect sender, SIMCONNECT_RECV data)
        {
            textBox_Connect.Text = "Disconnected";
            button_Connect.Text = "Connect";
            simconnect.Dispose();
            simconnect = null;
        }
 
        void simconnect_OnRecvOpen(SimConnect sender, SIMCONNECT_RECV_OPEN data)
        {
            textBox_Connect.Text = "Connected to FSX";
            button_Connect.Text = "Disconnect";
        }
 
        protected override void DefWndProc(ref Message m)
        {
            if (m.Msg == WM_USER_SIMCONNECT)
            {
                if (simconnect != null)
                {
                    simconnect.ReceiveMessage();
                }
            }
            else
            {
                base.DefWndProc(ref m);
            }
        }
    }
}

Labels: , ,

Wednesday, December 6, 2006

Updating from Work

A few quick hits:

  • I'm thinking about what would be better: longer, more in-depth tutorials that cover more ground and more detail, or short feature blogs that would take one step and then stop. Obviously, there are benefits to both methods. The short blogs could be posted more often, and would be easy to implement. The longer tutorials would have a greater sense of accomplishment and would have a greater lens on the fine details and inner workings of what you are doing. The purpose is, of course, not to just have you copy my code, but to understand why it works the way it works. The downside to the longer posts is that they probably would be less frequent than the daily updates I had planned on.
  • This project is completely open to suggestions. I have my own goals to accomplish with my programs, but this is separate from that. If you want to learn how to do something, let me know, and I'll incorporate it, assuming I can.
  • Icarus (my FSX add-on) is officially in-development. I even bought a big whiteboard to map everything out on. In case you don't know, Icarus is a program that replaces a good portion of the AI handling in FSX. Currently, I am hoping that I won't have to take over the actual flying of the planes (FSX actually does a rather good job of that), but mostly the way they are vectored via the ATC and how they act on the ground. This includes SIDS and STARS, multiple runways for takeoffs and landing (including crossing runways) better spacing (hopefully resulting in no go-arounds) and a number of other little features.
  • I will be looking for beta testers sometime early next year. The guys I really need are the people who have traffic packages (MyTrafficX, etc) installed. The goal is to accurately and smoothly integrate with those add-ons.
  • Although Icarus is going to be payware (it's a pretty complex undertaking), I am absolutely open to doing as many freeware tools as I can. First off, some of this stuff is not that difficult. Some is, but it's fun to make, so I'll do it anyways. Any ideas?
  • I definitely need beta testers soon for a project that I am almost finished with. It's called TrackerX, and it's basically a FlightAware.com style webpage for FSX. All you do is run a tiny little tray app while you fly, and it uploads your positional info to a database and displays it on a map with everyone else. So far, it's looking really cool, but once the webpage is close to finished, I want to be able to run with it.

That's all for now. Part Three of the tutorial will go up tonight, barring any craziness. I'll also be adding a link section on the sidebar for these tutorials, so you don't have to go digging around everywhere.

(P.S. - I like readers. Tell your friends )

Labels: , , ,

Friday, December 1, 2006

Engine Fires and Oil Leakage

Over at the Avsim SimConnect Forum, Endre brought up the question of getting engine fires and oil leaks to occur with SimConnect. He said that neither worked properly, so I set out to put together a program to show how it would indeed work as it should.

Problem is, it didn't.

I haven't tried this solution in C++, so it may just be an issue with the managed wrapper. But I think it's more likely that something doesn't work or we aren't implementing it right.

First off, the variables:

ENG ON FIRE:index On fire state
GENERAL ENG OIL LEAKED
PERCENT:index
Percent of max oil
capacity leaked


As you can see, ENG ON FIRE is a simple on/off switch for the failure (so it seems) and our oil var is just a percentage of how much oil has been lost. However, our second variable is read-only.

The ever-brilliant Pete Dowson ( http://www.schiratti.com/dowson.html ) has told us that most of the time in that case, we have to find an event that mirrors the effect. So far, I haven't found an event for either of these. Maybe someone has and can point me in the right direction. Because without an event and with the variable non-settable, we won't be able to do anything with it.

So the code is as follows (just for the engine part of things)

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;

// Include Simconnect and Interop References
// Remember to reference the SDK .DLL in the project
using Microsoft.FlightSimulator.SimConnect;
using System.Runtime.InteropServices;

namespace Engine_Fire
{
// Standard Windows Form, see the designer for placement
public partial class Form1 : Form
{
// SimConnect Object
SimConnect simconnect;

// User-defined win32 event
const int WM_USER_SIMCONNECT = 0x0402;

// Data definitions
enum DEFINITIONS
{
EngineData,
}

// Requests
enum DATA_REQUESTS
{
SET_ENGINE_FIRE,
}

// Our engine data struct.
// Standard marshalling message
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)]
struct EngineData
{
public bool engine1fire;
public bool engine2fire;
};

// Our initializer
// To-do: Initialize Simconnect object,
// Set up data definitions,
// Add struct to marshaller
public Form1()
{
InitializeComponent();

// Initialize SimConnect (this is straight from the SDK, just changed the app name)
simconnect = null;
try
{
simconnect = new SimConnect("Engine Fire Demo", this.Handle, WM_USER_SIMCONNECT, null, 0);
}
catch (COMException ex)
{
// A connection to the SimConnect server could not be established
// Normally you would handle the exception here
}

// Set up data definitions
simconnect.AddToDataDefinition(DEFINITIONS.EngineData, "Eng On Fire:0", "bool", SIMCONNECT_DATATYPE.FLOAT32, 0.0f, SimConnect.SIMCONNECT_UNUSED);
simconnect.AddToDataDefinition(DEFINITIONS.EngineData, "Eng On Fire:1", "bool", SIMCONNECT_DATATYPE.FLOAT32, 0.0f, SimConnect.SIMCONNECT_UNUSED);

// Add struct to marshaller
simconnect.RegisterDataDefineStruct(DEFINITIONS.EngineData);

}

// Default System Message Handler
protected override void DefWndProc(ref Message m)
{
if (m.Msg == WM_USER_SIMCONNECT)
{
if (simconnect != null)
{
simconnect.ReceiveMessage();
}
}
else
{
base.DefWndProc(ref m);
}
}

private void button1_Click(object sender, EventArgs e)
{
EngineData sendEngFire = new EngineData();

sendEngFire.engine1fire = true;
sendEngFire.engine2fire = true;
simconnect.SetDataOnSimObject(DEFINITIONS.EngineData, SimConnect.SIMCONNECT_OBJECT_ID_USER, SIMCONNECT_DATA_SET_FLAG.DEFAULT, sendEngFire);
}
}
}


The key is the button1_Click function. I slapped a button my form and all it's doing is sending a message to FSX to set the ENG ON FIRE vars for engines 1 and 2 to "True". So I fire up FSX, run the code, press the button,

and nothing. No response. I check the console window, and the data certainly is being sent.

Just to make sure something wasn't backwards, I ran a test to pull the variables instead of setting them, and in normal flight, they are set to false, even after trying to set them to "true" with my program.

Here's a shot "in the thick of it" (click for larger version)



Even after sending the variables, they still come back as false.

My assumption right now is that I'm doing it wrong, and I'm certainly open to suggestions.

Labels: , , , , ,