The online racing simulator
I discovered a bug in IS_REO in 1.5 beta. I have fixed it, but it haven't had time to make a full binary release, plus it's the only small change. If you plan to use IS_REO then you should update pyinsim.py and replace the current IS_REO with the following class.

class IS_REO:
def __init__(self, ReqI=0, NumP=0, PLID=[]):
self.Size = 36
self.Type = ISP_REO
self.ReqI = ReqI
self.NumP = NumP
self.PLID = PLID

def pack(self):
return struct.pack('BBBB32B', self.Size, self.Type, self.ReqI, self.NumP, *self.PLID)

def unpack(self, data):
self.Size, self.Type, self.ReqI, self.NumP = struct.unpack('BBBB', data[:4])
self.PLID = struct.unpack('32B', data[4:])

This fix will be in the next release.
Quote from DarkTimes :I discovered a bug in IS_REO in 1.5 beta...

Couple of things:
  • Why allow ReqI to be set in the packet's initialisation when the only way for ReqI to be something other than 0 is to receive a response to a TINY_REO? (of which unpack() will take care)
  • Why allow NumP to be set in the initialisation? It should be len(PLID) - PLID.count(0), setting it manually results in what I would call undefined behaviour.
I'd also recommend using the more efficient pre-compiled structs and ridding instruction packets of unpack() and info packets of pack(). My most recent definitions are attached (board doesn't allow .py extension).
Attached files
pyinsim_packet_defs.py.txt - 17.9 KB - 342 views
The packet classes were generated programatically by parsing InSim.txt, at some point I will need to manually revise each one, but I haven't had time to do so. Tweaking each packet is a lot of work and I have lots and lots of other things I could be doing. I will switch to precompiled structs at some point too I guess. It is a beta, it's not finished.
Found a few bugs in 1.5.3 beta. Note the bold text:


class IS_NLP:

...

self.Size, self.Type, self.ReqI, self.NumP = struct.unpack('BBBB', data[:4])
offset = 4
while offset < self.Size:
nl = NodeLap()
nl.unpack(data[offset:offset + [b]4][/b])
self.NodeLaps.append(nl)
offset += [b]4[/b]

A NodeLap struct is 6 bytes long... change the 4s to 6s and it's fixed.


class IS_RST:

...

def __init__(self, ReqI=0, RaceLaps=0, QualMins=0, NumP=0, Track='', Weather=0, Wind=0, Flags=0, NumNodes=0, Finish=0, Split1=0, Split2=0, Split3=0):

...

self.Zero = [b]Zero[/b]


class IS_MSX:

...

def __init__(self, ReqI=0, Msg=''):

...

self.Zero = [b]Zero[/b]


class IS_AXI:

...

def __init__(self, ReqI=0, AXStart=0, NumCP=0, NumO=0, LName=''):

...

self.Zero = [b]Zero[/b]

Zero is never defined so these assignments all throw exceptions.

Also, you might consider wrapping the code in onPacketReceived with a try/except block and printing the stack trace on exceptions. I don't know if it's something weird I'm doing and not realizing it, but my Pyinsim apps always fail silently when my Pyinsim-related code raises an exception. I figure it's either me being dumb or the threading module suppressing output to stderr.

Example of what I did:


import traceback

...

def onPacketReceived(self, data):
"""Virtual method called when a packet is received. Handles the keep
alive pulse and raises packet events.

@type data: string
@param data: Packet data as a binary formatted string.

"""
try:
packetType = _packetType(data)

# Keep alive.
if packetType == ISP_TINY:
if _tinyType(data) == TINY_NONE:
self.sendB(data)

# Raise packet event.
if packetType in self.__callbacks:
for callback, _customPacket in self.__callbacks[packetType]:
if _customPacket:
packet = _customPacket()
else:
packet = _PACKET_DEFS[packetType]()
packet.unpack(data)
callback(self, packet)
except:
traceback.print_exc()

OK - thanks for the bug reports on the beta, I'll fix those.

The packet receive code runs on a separate thread, so any exceptions which occur there happen on a different call-stack from your main program thread, which is why they appear to fail silently. To detect errors which happen on seperate threads in pyinsim you need to bind an event to EVT_ERROR.

def error(insim, err):
print 'Error:', err

insim.bind(pyinsim.EVT_ERROR, error)

Quote from DarkTimes :The packet receive code runs on a separate thread, so any exceptions which occur there happen on a different call-stack from your main program thread, which is why they appear to fail silently. To detect errors which happen on seperate threads in pyinsim you need to bind an event to EVT_ERROR.

def error(insim, err):
print 'Error:', err

insim.Bind(pyinsim.EVT_ERROR, error)


A ha! Thanks very much... I figured it was something like that but wasn't sure how to take care of it.
Found one more, also in IS_NLP. The bold code is my fix.


def unpack(self, data):
"""Unpack the packet data from a binary formatted string.

@type data: string
@param data: The packet data as a binary formatted string.

"""
self.Size, self.Type, self.ReqI, self.NumP = struct.unpack('BBBB', data[:4])
[b]lim = self.Size
if self.NumP % 2 == 1:
lim -= 2[/b]
offset = 4
while offset < [b]lim[/b]:
nl = NodeLap()
nl.unpack(data[offset:offset + 6])
self.NodeLaps.append(nl)
offset += 6

NodeLap packets are six bytes long, but InSim packets always have size divisible by 4. For an odd number of NodeLap structs, this means the packet is two bytes longer than the end of the last NodeLap struct. The last two empty bytes of the packet were getting passed to a new NodeLap for unpacking, which threw an exception.

For quick reference, here's the section from the InSim docs about IS_NLP:


struct IS_NLP // Node and Lap Packet - variable size
{
byte Size; // 4 + NumP * 6 (PLUS 2 if needed to make it a multiple of 4)
byte Type; // ISP_NLP
byte ReqI; // 0 unless this is a reply to an TINY_NLP request
byte NumP; // number of players in race

NodeLap Info[32]; // node and lap of each player, 1 to 32 of these (NumP)
};

Quote from morpha :
I'd also recommend using the more efficient pre-compiled structs and ridding instruction packets of unpack() and info packets of pack(). My most recent definitions are attached (board doesn't allow .py extension).

In addition to this, if you want to squeeze out even more performance (well... mostly just lower memory footprint), consider using __slots__. Note that you'd need to update your packet classes to new-style class definitions, but that would be an easy search-and-replace.

http://docs.python.org/reference/datamodel.html#slots

I think I'm done spamming the thread now.
Uploaded 1.5.5 beta.

Changes
  • Fixed bugs with IS_NLP, IS_RST, IS_AXI and IS_MSX noted above
  • Updated OutGauge to support LFS Z25 (including example and documentation)
Please note: I have decided to depreciate the old version of pyinsim in favor of the beta, as the beta is clearly much improved now, and so have removed the old download. Much of the example code in this thread is from the old version of the module and so is no longer supported. Please refer to the 'examples' folder within the distribution for up-to-date sample code. At some point I will try to update the code here when I can, but truthfully the examples included should be enough to get anyone up and running.

I have decided to leave the 'beta' moniker attatched to the module for the time being, as I am confident there may still be one or two small bugs lying around.
I've uploaded a new version of the distribution, as I made a silly mistake with the OutGauge example code and misunderstood a little how the new flags work.
You have a little fault in your example_2.py file... In line 32:

insim.bind(pyinsim.EVT_CL0SED, insimClosed)


EVT_CL0SED (there is a 'null' as O) isn't found in pyinsim.py. I had to change it to EVT_CLOSE (with a real 'O'), to get it working
Some of the packet definitions, while technically correct, can lead to unwanted (but to be expected) behaviour. Certain packets require the last byte of a string to be 0, the current packet definitions don't comply with this requirement in that they don't enforce it.

The affected packets are
  • IS_MST
  • IS_MSX
  • IS_MSL
  • IS_MTC
  • IS_RIP
  • IS_SSH
My suggestion is to reduce the string length by one and add a pad byte at the end of the struct format. IS_MST's pack() would then look like this
def pack(self):
"""Pack the packet values into a binary formatted string.

@rtype: string
@return: A binary formatted string.

"""
return struct.pack('4B63sx', self.Size, self.Type, self.ReqI, self.Zero, self.Msg)

Without this, LFS will drop the packet and throw a server side error message.
Pardon my ignornace, but I have a question.

I'm not a major serious coder or anything. However, I have been looking through and I'm not sure if I missed something, misunderstood something, or if it doesn't exist. Is there any way to get the license account username (ex I use the name !Learjet45 on servers, but my s2 license is learjet45. Is there a way to get the username (the learjet45 in my example) with pyinsim?

I have some previous experience with Python with the Sandbox mod for Battlefield 2 so that's why I chose pyinsim to experiment with. I understand some basics of python, but again, I am no serious major coder.
This is actually for a little code I want to experiment with that will take someone's username and various other bits of info (not really sure what yet :P ) and forward it to a PHP file that places the info in to a MySQL database.
My friend has given me the code he uses for the Sandbox mod to do a similar action, but I doubt that it is fully compatible with Pyinsim straight from Sandbox. So I am challenging myself to make it compatible with pyinsim.
Yes, but pyinsim itself is not a tracker like LFSLapper or AIRIO, which means while it is able to request and receive the information you seek, it does not do so on its own nor does it store it anywhere if you request it.

Perhaps you could post the code you have so we can help you port it to pyinsim?

BTW there is a MySQL module for python 2.6, MySQLdb (windows distros).
Yes, feel free to post the code and we can help you port it. In terms of InSim, the username (license name) is sent in the IS_NCN packet when a player joins the host.

This code for example prints out the license name of each player who joins a host.

import pyinsim

def connectionJoined(insim, ncn):
print 'Username', ncn.UName, 'joined host'

insim = pyinsim.insimConnect('localhost', 29999, IName='^3pyinsim')
insim.bind(pyinsim.ISP_NCN, connectionJoined)
insim.run()

Quote from morpha :Yes, but pyinsim itself is not a tracker like LFSLapper or AIRIO, which means while it is able to request and receive the information you seek, it does not do so on its own nor does it store it anywhere if you request it.

Perhaps you could post the code you have so we can help you port it to pyinsim?

BTW there is a MySQL module for python 2.6, MySQLdb (windows distros).

Thanks, and I'll see what I can do on my own first. Plus I need permission first to be able to post the code.

Quote from DarkTimes :Yes, feel free to post the code and we can help you port it. In terms of InSim, the username (license name) is sent in the IS_NCN packet when a player joins the host.

This code for example prints out the license name of each player who joins a host.

import pyinsim

def connectionJoined(insim, ncn):
print 'Username', ncn.UName, 'joined host'

insim = pyinsim.insimConnect('localhost', 29999, IName='^3pyinsim')
insim.bind(pyinsim.ISP_NCN, connectionJoined)
insim.run()


Thanks for the sample code! That will help me a lot! And again, I'll wait and see what I can do on my own first before I post the code.
Well from what I have of my basic test code (connects to the MySQL database, searches for my username, and sends back info if it is found, next part varies depending on if it is found), and so far so good. I got rid of all my errors with the code and it will run just fine. But for some reason I have to use 192.168.1.104 for my IP for insim instead of localhost, I get the error that insim couldnt connect if I use localhost.
Anyways, I don't know what's up, but as soon as I join my server, insim shuts off basically.
It happens with the sample codes too. As soon as the event happens once (ex a racer speeds over 80 km/h in example 4 i believe), insim closes the connection and doesn't run anymore. How do I keep insim alive so it doesn't shut off?
import the modules traceback and time and wrap the entire code from below the imports down to the last line in
try:

<code>

except:
traceback.print_exc()
time.sleep(10)

That should give you enough time to read the exception. It'd still be easier for us to help if you could post the code
I still gotta hear back from my friend who gave me the code.
In the mean time, I'll try it. Thanks!
Btw Alex, an easy way to compile your __all__ dict is using dir() and filtering all names starting with an underscore, provided that's a consistent pattern you used for private variables, which I believe you did.

My modified pyinsim, which I shall release soon-ish, currently counts 395 public names. It's using precompiled structs and regexes, __slots__ as suggested by tmehlinger and sorted callbacks (sometimes a callback needs to be first or last to be called).
I'm working on the UDP part now, particularly mixed mode where NLP/MCI is sent via UDP and the rest via TCP. If you or anyone else wants to have a peek, I could upload the current version, but as it is now, UDP is not working at all so I'd rather wait till that's fixed.
OK cool - I'll look forward to it.

I am currently working on a new version that uses pre-compiled structs, __slots__, and supports Python 3.0. I'm also rewriting the API a little to simplify it further and add more features. I don't have a lot of time for it at the moment, which is why it's taking a while.

Quote :sorted callbacks (sometimes a callback needs to be first or last to be called)

Callbacks are currently called in the order they are bound, if you want a callback called last just bind it last.
Quote from DarkTimes :and supports Python 3.0.

Interesting, I'd like to get into Python 3 but it'll take some getting used to and I'll have to make sure all modules I need are available... MySQLdb most certainly is not :sadbanana

Quote from DarkTimes :I'm also rewriting the API a little to simplify it further and add more features. I don't have a lot of time for it at the moment, which is why it's taking a while.

Perhaps I should wait for your version then

Quote from DarkTimes :Callbacks are currently called in the order they are bound, if you want a callback called last just bind it last.

Well it's not that easy once your application grows, especially if it unbinds and rebinds alot. My solution is simple (although it might not look like it :razz and lightweight:
def bind(self, evtType, callback, priority = None):
"""Bind a packet callback event-handler.

@type evtType: enum
@param evtType: Type of evt to bind.
@type callback: function
@param callback: Function to call when the event is raised.
@type priority: number
@param priority: CBPRIO_FIRST ensures the callback is called first upon receiving a packet of
evtType, CBPRIO_LAST ensures it is called last. There can only be one FIRST and LAST per evtType and
it's always the most recent priority entry.

"""
# No callbacks for that type, priority irrelevant.
if not evtType in self.__callbacks:
self.__callbacks[evtType] = []
self.__callbacks[evtType].append((callback, priority))
# Priority FIRST, this callback wants to be called first upon receiving a packet of evtType.
elif priority == CBPRIO_FIRST:
self.__callbacks[evtType][0] = (self.__callbacks[evtType][0][0], None)
self.__callbacks[evtType].insert(0,(callback, priority))
# Priority LAST, this callback wants to be called after all others.
elif priority == CBPRIO_LAST:
self.__callbacks[evtType][-1] = (self.__callbacks[evtType][-1][0], None)
self.__callbacks[evtType].append((callback, priority))
# If a LAST priority is set on the cb stack, pop it back to the end of the stack.
elif self.__callbacks[evtType][-1][1] == CBPRIO_LAST:
self.__callbacks[evtType].extend([(callback, priority), self.__callbacks[evtType].pop()])
else:
self.__callbacks[evtType].append((callback, priority))

Basically it will make sure that a handler bound with CBPRIO_FIRST will always be the first to be called, unless a second handler requests CBPRIO_FIRST, in which case it should probably throw an exception, but I left that out since I can't think of a situation where the solution would be anything other than ignoring it. Same applies to CBPRIO_LAST being the last to be called, obviously.

Edit: After reading the most significant changes I'm inclined to give it a try, even though, as expected, there is no MySQLdb for it, which is one of the most important modules for my app.
It took me ages to get around to trying Python 3.0, but it's not that big a change on the surface really. A lot of things make more sense and most of the significant changes can be learnt in half an hour. The biggest change though, which affects pyinsim the most, is that all strings are now unicode and there is a new byte data-type. As pyinsim deals with strings of data, all that has to be rewritten to use the new type. Also as all strings are now unicode, pyinsim needs to be able to convert LFS strings into unicode and then back again, when packing and unpacking packets. Dealing with LFS strings is a horrible nightmare. But aside from that it's coming along OK.
Alright. I need some help now.
LFS closes the connection to insim as soon as the I connect. PM me for the file, I don't want it out in the open and neither does my friend who wrote the original code this is based on.

FGED GREDG RDFGDR GSFDG