Well, I’ve been away for a while, and still don’t have enough time to do anything helpful, but as my personal circumstances have changed recently I thought I would document some of the stuff that I know about Steam, in the hope that it helps others in their work. Sorry if it's a bit rushed - I knocked it up pretty quickly, but wanted to start filling out the Developer forum with some more useful stuff Smile Please note that I’ve written most of the code directly in this thread, as such it is largely untested and any corrections for typos/ general stupidity are welcome Smile I have also re-uploaded all of my original tutorials on building a Steam Launcher in C# and you can find them here: http://cs.rin.ru/forum/viewtopic.php?t=43463 A lot of the documentation in this thread is also covered in the latter parts of the tutorials which detail integrating Steam content into the launcher. As usual, all comments / suggestions are most welcome Wink, but please bear in mind that i'm not around as much as I used to be Very Happy Steam API revealed Introduction This thread is intended to document steam function calls and give practical examples of how they can be used in your own projects. This documentation is applicable to interfacing with a legitimate valve steam.dll file, as well as a steam emulator like SteamEmu by hCUPa. Where differences arise, then these will be explained further. I will add to this as and when I can, and would also encourage others to participate in this thread if they can expand on any of the documentation. It is assumed that you have a basic understanding of C#/VB.NET and calling dll functions from your programs. This documentation will not explain variable types, calling conventions or any other basic programming methodology. If you do not have basic programming skills then please read my earlier tutorials on building a content launcher in C# and integrating it with Steam: http://cs.rin.ru/forum/viewtopic.php?t=43463 Getting started Using the functions contained in steam.dll is pretty much the same as using the functions in any other windows dll. However, there are some basic structures and enums that you should always declare to make your life easier. I will not detail everything in one go, but rather add to this thread as we go along. It is worth noting at this point that SteamEmu by hCUPa is a very complete steam emulator, so much so that we can actually replace our legitimate steam dll with the SteamEmu dll and our code will still work. There are some exceptions, and even some additions to the things we can do with SteamEmu, and where relevant, they will be highlighted. Before we start, note that you will need the following namespaces for the code below to compile: Code: C# -------------------------------------------------------------------------- ------- using System; using System.Collections.Generic; using System.Windows.Forms; using System.Text; using System.Runtime.InteropServices; VB -------------------------------------------------------------------------- ------- Imports System Imports System.Collections.Generic Imports System.Windows.Forms Imports System.Text Imports System.Runtime.InteropServices You will also need to set your compiler to compile as x86 code, particularly if you are deloping within a 64bit environment, as we are going to be accessing a 32bit dll. Steam error handling Like all things in programming, let’s start with errors! We need to define some stuff to be able to create a Steam error object and report Steam errors as and when they occur. First we need to create some enums to allow us to easily see what errors occurred in a textual and descriptive way. Code: C# -------------------------------------------------------------------------- ------- public enum eSteamError { eSteamErrorNone = 0, eSteamErrorUnknown = 1, eSteamErrorLibraryNotInitialized = 2, eSteamErrorLibraryAlreadyInitialized = 3, eSteamErrorConfig = 4, eSteamErrorContentServerConnect = 5, eSteamErrorBadHandle = 6, eSteamErrorHandlesExhausted = 7, eSteamErrorBadArg = 8, eSteamErrorNotFound = 9, eSteamErrorRead = 10, eSteamErrorEOF = 11, eSteamErrorSeek = 12, eSteamErrorCannotWriteNonUserConfigFile = 13, eSteamErrorCacheOpen = 14, eSteamErrorCacheRead = 15, eSteamErrorCacheCorrupted = 16, eSteamErrorCacheWrite = 17, eSteamErrorCacheSession = 18, eSteamErrorCacheInternal = 19, eSteamErrorCacheBadApp = 20, eSteamErrorCacheVersion = 21, eSteamErrorCacheBadFingerPrint = 22, eSteamErrorNotFinishedProcessing = 23, eSteamErrorNothingToDo = 24, eSteamErrorCorruptEncryptedUserIDTicket = 25, eSteamErrorSocketLibraryNotInitialized = 26, eSteamErrorFailedToConnectToUserIDTicketValidationServer = 27, eSteamErrorBadProtocolVersion = 28, eSteamErrorReplayedUserIDTicketFromClient = 29, eSteamErrorReceiveResultBufferTooSmall = 30, eSteamErrorSendFailed = 31, eSteamErrorReceiveFailed = 32, eSteamErrorReplayedReplyFromUserIDTicketValidationServer = 33, eSteamErrorBadSignatureFromUserIDTicketValidationServer = 34, eSteamErrorValidationStalledSoAborted = 35, eSteamErrorInvalidUserIDTicket = 36, eSteamErrorClientLoginRateTooHigh = 37, eSteamErrorClientWasNeverValidated = 38, eSteamErrorInternalSendBufferTooSmall = 39, eSteamErrorInternalReceiveBufferTooSmall = 40, eSteamErrorUserTicketExpired = 41, eSteamErrorCDKeyAlreadyInUseOnAnotherClient = 42, eSteamErrorNotLoggedIn = 101, eSteamErrorAlreadyExists = 102, eSteamErrorAlreadySubscribed = 103, eSteamErrorNotSubscribed = 104, eSteamErrorAccessDenied = 105, eSteamErrorFailedToCreateCacheFile = 106, eSteamErrorCallStalledSoAborted = 107, eSteamErrorEngineNotRunning = 108, eSteamErrorEngineConnectionLost = 109, eSteamErrorLoginFailed = 110, eSteamErrorAccountPending = 111, eSteamErrorCacheWasMissingRetry = 112, eSteamErrorLocalTimeIncorrect = 113, eSteamErrorNetwork = 200 } VB -------------------------------------------------------------------------- ------- Public Enum eSteamError eSteamErrorNone = 0 eSteamErrorUnknown = 1 eSteamErrorLibraryNotInitialized = 2 eSteamErrorLibraryAlreadyInitialized = 3 eSteamErrorConfig = 4 eSteamErrorContentServerConnect = 5 eSteamErrorBadHandle = 6 eSteamErrorHandlesExhausted = 7 eSteamErrorBadArg = 8 eSteamErrorNotFound = 9 eSteamErrorRead = 10 eSteamErrorEOF = 11 eSteamErrorSeek = 12 eSteamErrorCannotWriteNonUserConfigFile = 13 eSteamErrorCacheOpen = 14 eSteamErrorCacheRead = 15 eSteamErrorCacheCorrupted = 16 eSteamErrorCacheWrite = 17 eSteamErrorCacheSession = 18 eSteamErrorCacheInternal = 19 eSteamErrorCacheBadApp = 20 eSteamErrorCacheVersion = 21 eSteamErrorCacheBadFingerPrint = 22 eSteamErrorNotFinishedProcessing = 23 eSteamErrorNothingToDo = 24 eSteamErrorCorruptEncryptedUserIDTicket = 25 eSteamErrorSocketLibraryNotInitialized = 26 eSteamErrorFailedToConnectToUserIDTicketValidationServer = 27 eSteamErrorBadProtocolVersion = 28 eSteamErrorReplayedUserIDTicketFromClient = 29 eSteamErrorReceiveResultBufferTooSmall = 30 eSteamErrorSendFailed = 31 eSteamErrorReceiveFailed = 32 eSteamErrorReplayedReplyFromUserIDTicketValidationServer = 33 eSteamErrorBadSignatureFromUserIDTicketValidationServer = 34 eSteamErrorValidationStalledSoAborted = 35 eSteamErrorInvalidUserIDTicket = 36 eSteamErrorClientLoginRateTooHigh = 37 eSteamErrorClientWasNeverValidated = 38 eSteamErrorInternalSendBufferTooSmall = 39 eSteamErrorInternalReceiveBufferTooSmall = 40 eSteamErrorUserTicketExpired = 41 eSteamErrorCDKeyAlreadyInUseOnAnotherClient = 42 eSteamErrorNotLoggedIn = 101 eSteamErrorAlreadyExists = 102 eSteamErrorAlreadySubscribed = 103 eSteamErrorNotSubscribed = 104 eSteamErrorAccessDenied = 105 eSteamErrorFailedToCreateCacheFile = 106 eSteamErrorCallStalledSoAborted = 107 eSteamErrorEngineNotRunning = 108 eSteamErrorEngineConnectionLost = 109 eSteamErrorLoginFailed = 110 eSteamErrorAccountPending = 111 eSteamErrorCacheWasMissingRetry = 112 eSteamErrorLocalTimeIncorrect = 113 eSteamErrorNetwork = 200 End Enum Code: C# -------------------------------------------------------------------------- ------- public enum eDetailedPlatformErrorType { eNoDetailedErrorAvailable, eStandardCerrno, eWin32LastError, eWinSockLastError, eDetailedPlatformErrorCount } VB -------------------------------------------------------------------------- ------- Public Enum EDetailedPlatformErrorType eNoDetailedErrorAvailable eStandardCerrno eWin32LastError eWinSockLastError eDetailedPlatformErrorCount End Enum After we have declared our enums, we can create an error structure in code with the following: Code: C# -------------------------------------------------------------------------- ------- [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 4)] public struct TSteamError { public eSteamError eSteamError; public eDetailedPlatformErrorType eDetailedErrorType; public uint ErrCode; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 255)] public string ErrDescription; } VB -------------------------------------------------------------------------- ------- _ Public Structure TSteamError Public eSteamError As eSteamError Public eDetailedErrorType As EDetailedPlatformErrorType Public ErrCode As UInteger Public ErrDescription As String End Structure When we have this structure in place, we can then create an error variable within our program like so: Code: C# -------------------------------------------------------------------------- ------- TSteamError SErr = new TSteamError(); VB -------------------------------------------------------------------------- ------- Dim SErr As New TSteamError() Steam log-in functions So, now we can report errors back to our program, let’s look at how we can actually connect with the Steam network. This is one thing that we can do differently in Steam Emu, however first let’s see how we would Log-In to a legitimate Steam account. To do the Login, we need to add another structure: Code: C# -------------------------------------------------------------------------- ------- [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 4)] public struct TSteamProgress { public int Valid; public int Percent; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 255)] public string Progress; } VB -------------------------------------------------------------------------- ------- _ Public Structure TSteamProgress Public Valid As Integer Public Percent As Integer Public Progress As String End Structure And use the following functions: Code: C# -------------------------------------------------------------------------- ------- [DllImport("steam.dll", CallingConvention=CallingConvention.Cdecl)] public static extern int SteamStartEngine(ref TSteamError SteamErr); [DllImport("steam.dll", CallingConvention=CallingConvention.Cdecl)] public static extern int SteamStartup(uint UsingMask, ref TSteamError SteamErr); [DllImport("steam.dll", CallingConvention=CallingConvention.Cdecl)] public static extern uint SteamLogin(string User, string Password, int isSecureComputer, ref TSteamError SteamErr); [DllImport("steam.dll", CallingConvention=CallingConvention.Cdecl)] public static extern uint SteamSetUser(string User, ref int UserSet, ref TSteamError SteamErr); [DllImport("steam.dll", CallingConvention=CallingConvention.Cdecl)] public static extern int SteamProcessCall(uint SteamHandle, ref TSteamProgress Progress, ref TSteamError SteamErr); [DllImport("steam.dll", CallingConvention=CallingConvention.Cdecl)] public static extern int SteamIsLoggedIn(ref int isLoggedIn, ref TSteamError SteamErr); VB -------------------------------------------------------------------------- ------- _ Public Function SteamStartEngine(ByRef SteamErr As TSteamError) As Integer End Function _ Public Function SteamStartup(ByVal UsingMask As UInteger, ByRef SteamErr As TSteamError) As Integer End Function _ Public Function SteamLogin(ByVal User As String, ByVal Password As String, ByVal isSecureComputer As Integer, ByRef SteamErr As TSteamError) As UInteger End Function _ Public Function SteamSetUser(ByVal User As String, ByRef UserSet As Integer, ByRef SteamErr As TSteamError) As UInteger End Function _ Public Function SteamProcessCall(ByVal SteamHandle As UInteger, ByRef Progress As TSteamProgress, ByRef SteamError As TSteamError) As Integer End Function _ Public Function SteamIsLoggedIn(ByRef isLoggedIn As Integer, ByRef SteamErr As TSteamError) As Integer End Function And to cleanup afterwards, we will also need these functions: Code: C# -------------------------------------------------------------------------- ------- [DllImport("steam.dll", CallingConvention=CallingConvention.Cdecl)] public static extern uint SteamLogout(ref TSteamError SteamErr); [DllImport("steam.dll", CallingConvention=CallingConvention.Cdecl)] public static extern int SteamCleanup(ref TSteamError SteamErr); [DllImport("steam.dll", CallingConvention=CallingConvention.Cdecl)] public static extern int SteamShutdownEngine(ref TSteamError SteamErr); VB -------------------------------------------------------------------------- ------- _ Public Function SteamLogout(ByRef SteamErr As TSteamError) As UInteger End Function _ Public Function SteamCleanup(ByRef SteamErr As TSteamError) As Integer End Function _ Public Function SteamShutdownEngine(ByRef SteamErr As TSteamError) As Integer End Function After declaring the above function calls, we can login to Steam using something like this: Code: C# -------------------------------------------------------------------------- ------- TSteamError SErr = new TSteamError(); int iSteamReturn = 0; int iUserSet = 0; int isLoggedIn = 0; string SteamUserName = "MyUserName"; string SteamUserPassword = "MyPassword"; uint sHandle; uint startoptions = 15; iSteamReturn = SteamStartEngine(ref SErr); iSteamReturn = SteamStartup(startoptions, ref SErr); TSteamProgress TProgress = new TSteamProgress(); TProgress.Valid = 1; sHandle = SteamLogin(SteamUserName, SteamUserPassword, 1, ref SErr); while (SteamProcessCall(sHandle, ref TProgress, ref SErr) == 0) { Application.DoEvents(); } iSteamReturn = SteamIsLoggedIn (ref isLoggedIn, ref SErr); if (isLoggedIn == 1) { sHandle = SteamSetUser(SteamUserName, ref iUserSet, ref SErr); TProgress = new TSteamProgress(); TProgress.Valid = 1; while (SteamProcessCall(sHandle, ref TProgress, ref SErr) == 0) { Application.DoEvents(); } if (iUserSet == 1) { //Do Something! } } sHandle = SteamLogout(ref SErr); TProgress = new TSteamProgress(); TProgress.Valid = 1; while (SteamProcessCall(sHandle, ref TProgress, ref SErr) == 0) { Application.DoEvents(); } iSteamReturn = SteamCleanup(ref SErr); iSteamReturn = SteamShutdownEngine(ref SErr); VB -------------------------------------------------------------------------- ------- Dim SErr As New TSteamError() Dim iSteamReturn As Integer = 0 Dim iUserSet As Integer = 0 Dim isLoggedIn As Integer = 0 Dim SteamUserName As String = "MyUserName" Dim SteamUserPassword As String = "MyPassword" Dim sHandle As UInteger Dim startoptions As UInteger = 15 iSteamReturn = SteamStartEngine(SErr) iSteamReturn = SteamStartup(startoptions, SErr) Dim TProgress As New TSteamProgress() TProgress.Valid = 1 sHandle = SteamLogin(SteamUserName, SteamUserPassword, 1, SErr) While SteamProcessCall(sHandle, TProgress, SErr) = 0 Application.DoEvents() End While iSteamReturn = SteamIsLoggedIn(isLoggedIn, SErr) If isLoggedIn = 1 Then sHandle = SteamSetUser(SteamUserName, iUserSet, SErr) TProgress = New TSteamProgress() TProgress.Valid = 1 While SteamProcessCall(sHandle, TProgress, SErr) = 0 Application.DoEvents() End While If iUserSet = 1 Then 'Do Something! End If End If sHandle = SteamLogout(SErr) TProgress = New TSteamProgress() TProgress.Valid = 1 While SteamProcessCall(sHandle, TProgress, SErr) = 0 Application.DoEvents() End While iSteamReturn = SteamCleanup(SErr) iSteamReturn = SteamShutdownEngine(SErr) To show the code flow, I have left out any relevant error handling, but in the real world you would add a check on your Steam error variable whenever it is passed to an external function like so, and break appropriately: Code: C# -------------------------------------------------------------------------- ------- iSteamReturn = SteamStartEngine(ref SErr); if (SErr.eSteamError != eSteamError.eSteamErrorNone ) { MessageBox.Show("Steam Error : " + SErr.ErrDescription); } VB -------------------------------------------------------------------------- ------- iSteamReturn = SteamStartEngine(SErr) If SErr.eSteamError <> eSteamError.eSteamErrorNone Then MessageBox.Show("Steam Error : " + SErr.ErrDescription) End If Now, if you aren’t bothered about logging into steam correctly, and just want to use SteamEmu, then using SteamSetUser on its own will suffice, however it is worth noting that SteamEmu will process all of the above calls correctly, so you could use either method. You must also take care if using SteamSetUser on its own to ensure you start the SteamEngine seperately in your code, whenever it is required. Steam application functions Ok, so now we can log in to either Steam (via a valid account) or SteamEmu, let’s see how we can get various information about Steam content out of the clientregistry.blob file. First we need a structure that returns information about the clientregistry.blob, we can use this to determine how many apps are in the blob file and dimension our arrays accordingly: Code: C# -------------------------------------------------------------------------- ------- [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 4)] public struct TSteamAppStats { public uint NumApps; public uint MaxNameChars; public uint MaxVersionLabelChars; public uint MaxLaunchOptions; public uint MaxLaunchOptionDescChars; public uint MaxLaunchOptionCmdLineChars; public uint MaxNumIcons; public uint MaxIconSize; } VB -------------------------------------------------------------------------- ------- _ Public Structure TSteamAppStats Public NumApps As UInteger Public MaxNameChars As UInteger Public MaxVersionLabelChars As UInteger Public MaxLaunchOptions As UInteger Public MaxLaunchOptionDescChars As UInteger Public MaxLaunchOptionCmdLineChars As UInteger Public MaxNumIcons As UInteger Public MaxIconSize As UInteger End Structure Now we need an object to hold information about a specific app, like so: Code: C# -------------------------------------------------------------------------- ------- [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 4)] public struct TSteamApp { public string Name; public uint maxNameChars; public string latestVersionLabel; public uint maxLatestVersionLabelChars; public string currentVersionLabel; public uint maxCurrentVersionLabelChars; public string cacheFile; public uint maxCacheFileChars; public uint id; public uint latestVersionId; public uint currentVersionId; public uint minCacheFileSizeMB; public uint maxCacheFileSizeMB; public uint numLaunchOptions; public uint numIcons; public uint numVersions; public uint numDependencies; } VB -------------------------------------------------------------------------- ------- _ Public Structure TSteamApp Public Name As String Public maxNameChars As UInteger Public latestVersionLabel As String Public maxLatestVersionLabelChars As UInteger Public currentVersionLabel As String Public maxCurrentVersionLabelChars As UInteger Public cacheFile As String Public maxCacheFileChars As UInteger Public id As UInteger Public latestVersionId As UInteger Public currentVersionId As UInteger Public minCacheFileSizeMB As UInteger Public maxCacheFileSizeMB As UInteger Public numLaunchOptions As UInteger Public numIcons As UInteger Public numVersions As UInteger Public numDependencies As UInteger End Structure If we then declare the following functions: Code: C# -------------------------------------------------------------------------- ------- [DllImport("steam.dll", CallingConvention=CallingConvention.Cdecl)] public static extern int SteamGetAppStats(ref TSteamAppStats AppStats, ref TSteamError SteamErr); VB -------------------------------------------------------------------------- ------- _ Public Function SteamGetAppStats(ByRef AppStats As TSteamAppStats, ByRef SteamErr As TSteamError) As Integer End Function The SteamGetAppStats call returns a TSteamAppStats object that tells us how many AppIDs are available, amongst other things. Code: C# -------------------------------------------------------------------------- ------- [DllImport("steam.dll", CallingConvention=CallingConvention.Cdecl)] public static extern int SteamGetAppIds(uint[] AppIDs, uint MaxAppIDs, ref TSteamError SteamErr); VB -------------------------------------------------------------------------- ------- _ Public Function SteamGetAppIds(ByVal AppIDs() As UInteger, ByVal MaxAppIDs As UInteger, ByRef SteamErr As TSteamError) As Integer End Function The SteamGetAppIds call returns an array of all the AppIDs Code: C# -------------------------------------------------------------------------- ------- [DllImport("steam.dll", CallingConvention=CallingConvention.Cdecl)] public static extern int SteamEnumerateApp(uint AppID, ref TSteamApp AppStruc, ref TSteamError SteamErr); VB -------------------------------------------------------------------------- ------- _ Public Function SteamEnumerateApp(ByVal Appid As UInteger, ByRef ByvalAppStruc As TSteamApp, ByRef SteamErr As TSteamError) As Integer End Function The SteamEnumerateApp call returns basic information about the queried AppID We are now in a position to enumerate all the apps in the local clientregistry.blob file and retrieve information from them like so: Code: C# -------------------------------------------------------------------------- ------- TSteamError SErr = new TSteamError(); int iSteamReturn = 0; TSteamAppStats AppStats = new TSteamAppStats(); iSteamReturn = SteamGetAppStats(ref AppStats, ref SErr); if (AppStats.NumApps > 0) { uint[] RawAppIDs; RawAppIDs = new uint[AppStats.NumApps]; iSteamReturn = SteamGetAppIds(RawAppIDs, AppStats.NumApps, ref SErr); foreach (uint RawAppID in RawAppIDs) { TSteamApp AppNFO = new TSteamApp(); AppNFO.Name = new String(' ', 255); AppNFO.maxNameChars = 255; AppNFO.currentVersionLabel = new String(' ', 255); AppNFO.maxCurrentVersionLabelChars = 255; AppNFO.latestVersionLabel = new String(' ', 255); AppNFO.maxLatestVersionLabelChars = 255; AppNFO.cacheFile = new String(' ', 255); AppNFO.maxCacheFileChars = 255; iSteamReturn = SteamEnumerateApp(RawAppID, ref AppNFO, ref SErr); //Do Something! } } VB -------------------------------------------------------------------------- ------- Dim SErr As New TSteamError() Dim iSteamReturn As Integer = 0 Dim AppStats As New TSteamAppStats() iSteamReturn = SteamGetAppStats(AppStats, SErr) If AppStats.NumApps > 0 Then Dim RawAppIDs As UInteger() RawAppIDs = New UInteger(AppStats.NumApps - 1) {} iSteamReturn = SteamGetAppIds(RawAppIDs, AppStats.NumApps, SErr) For Each RawAppID As UInteger In RawAppIDs Dim AppNFO As New TSteamApp() AppNFO.Name = New String(" "c, 255) AppNFO.maxNameChars = 255 AppNFO.currentVersionLabel = New String(" "c, 255) AppNFO.maxCurrentVersionLabelChars = 255 AppNFO.latestVersionLabel = New String(" "c, 255) AppNFO.maxLatestVersionLabelChars = 255 AppNFO.cacheFile = New String(" "c, 255) AppNFO.maxCacheFileChars = 255 iSteamReturn = SteamEnumerateApp(RawAppID, AppNFO, SErr) 'Do Something! Next End If Ok, so now we can enumerate all the AppIDs in the clientregistry.blob file, what other functions can we actually use whilst processing them. First we can tell if an App is subscribed using the following function, this is handy if you’re writing a legitimate launcher: Code: C# -------------------------------------------------------------------------- ------- [DllImport("steam.dll", CallingConvention=CallingConvention.Cdecl)] public static extern int SteamIsAppSubscribed(uint AppID, ref uint IsAppSubscribed, ref uint IsAppReady, ref TSteamError SteamErr); VB -------------------------------------------------------------------------- ------- _ Public Function SteamIsAppSubscribed(ByVal appid As UInteger, ByRef IsAppSubscribed As UInteger, ByRef IsAppReady As UInteger, ByRef SteamErr As TSteamError) As Integer End Function We can call it like the following example, where we are testing to see if a subscription exists for Half-Life: Code: C# -------------------------------------------------------------------------- ------- TSteamError SErr = new TSteamError(); int iSteamReturn = 0; uint isAppSubscribed = 0; uint isAppReady = 0; uint AppID = 70; iSteamReturn = SteamIsAppSubscribed(AppID, ref isAppSubscribed, ref isAppReady, ref SErr); if (isAppSubscribed == 1 && isAppReady == 1) { //Do Something! } VB -------------------------------------------------------------------------- ------- Dim SErr As New TSteamError() Dim iSteamReturn As Integer = 0 Dim isAppSubscribed As UInteger = 0 Dim isAppReady As UInteger = 0 Dim AppID As UInteger = 70 iSteamReturn = SteamIsAppSubscribed(AppID, isAppSubscribed, isAppReady, SErr) If isAppSubscribed = 1 AndAlso isAppReady = 1 Then 'Do Something! End If If the App is subscribed then we can easily launch it with the following: Code: C# -------------------------------------------------------------------------- ------- [DllImport("steam.dll", CallingConvention=CallingConvention.Cdecl)] public static extern uint SteamLaunchApp(uint AppId, uint opt, string Args, ref TSteamError SteamErr); VB -------------------------------------------------------------------------- ------- _ Public Function SteamLaunchApp(ByVal appid As UInteger, ByVal opt As UInteger, ByVal Args As String, ByRef SteamErr As TSteamError) As UInteger End Function Which we call like so: Code: C# -------------------------------------------------------------------------- ------- TSteamError SErr = new TSteamError(); uint sHandle = 0; uint AppID = 70; string sArgs = " -console"; sHandle = SteamLaunchApp(AppID, 0, sArgs, ref SErr); VB -------------------------------------------------------------------------- ------- Dim SErr As New TSteamError() Dim sHandle As UInteger = 0 Dim AppID As UInteger = 70 Dim sArgs As String = " -console" sHandle = SteamLaunchApp(AppID, 0, sArgs, SErr) SteamLaunchApp is a great way of launching any steam content, because it will do all of the following for you: Extract minimum footprint files if they do not exist Refresh minimum footprint files if they are outdated from the GCF Extract additional dependencies if needed (e.g. the Steam media Player) Returns a handle to allow code to be written that waits until the App has finished Launch the content with optional arguments Though SteamLaunchApp will launch the associated AppID, there are some apps in Steam that have multiple launch options, like a config screen for example. To get these we need to enumerate the AppID’s Launch options, and for this we need another structure: Code: C# -------------------------------------------------------------------------- ------- [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 4)] public struct TSteamAppLaunchOption { public string description; public uint maxDescChars; public string cmdLine; public uint maxCmdLineChars; public uint index; public uint iconIndex; public uint noDesktopShortcut; public uint noStartMenuShortcut; public uint isLongRunningUnattended; } VB -------------------------------------------------------------------------- ------- _ Public Structure TSteamAppLaunchOption Public description As String Public maxDescChars As UInteger Public cmdLine As String Public maxCmdLineChars As UInteger Public index As UInteger Public iconIndex As UInteger Public noDesktopShortcut As UInteger Public noStartMenuShortcut As UInteger Public isLongRunningUnattended As UInteger End Structure And also, the following function call to populate the structure: Code: C# -------------------------------------------------------------------------- ------- [DllImport("steam.dll", CallingConvention=CallingConvention.Cdecl)] public static extern int SteamEnumerateAppLaunchOption(uint Appid, uint LaunchIdx, ref TSteamAppLaunchOption SteamAppLaunchOption, ref TSteamError SteamErr); VB -------------------------------------------------------------------------- ------- _ Public Function SteamEnumerateAppLaunchOption(ByVal Appid As UInteger, ByVal LaunchIdx As UInteger, ByRef SteamAppLaunchOption As TSteamAppLaunchOption, ByRef SteamErr As TSteamError) As Integer End Function We can now do something like this: Using AppNFO from our SteamEnumerateApp example above: Code: C# -------------------------------------------------------------------------- ------- if (AppNFO.numLaunchOptions > 1) { for (uint iloop = 1; iloop < AppNFO.numLaunchOptions; iloop++) { AppLO.description = new String(' ', 255); AppLO.maxDescChars = 255; AppLO.cmdLine = new String(' ', 255); AppLO.maxCmdLineChars = 255; AppLO.index = 0; AppLO.iconIndex = 0; AppLO.noDesktopShortcut = 0; AppLO.noStartMenuShortcut = 0; AppLO.isLongRunningUnattended = 0; iSteamReturn = SteamEnumerateAppLaunchOption(AppID, iloop, ref AppLO, ref SErr); //Do Something! } } VB -------------------------------------------------------------------------- ------- If AppNFO.numLaunchOptions > 1 Then For iloop As UInteger = 1 To AppNFO.numLaunchOptions - 1 Dim AppLO As New TSteamAppLaunchOption() AppLO.description = New String(" "c, 255) AppLO.maxDescChars = 255 AppLO.cmdLine = New String(" "c, 255) AppLO.maxCmdLineChars = 255 AppLO.index = 0 AppLO.iconIndex = 0 AppLO.noDesktopShortcut = 0 AppLO.noStartMenuShortcut = 0 AppLO.isLongRunningUnattended = 0 iSteamReturn = SteamEnumerateAppLaunchOption(AppID, iloop, AppLO, SErr) 'Do Something! Next End If We can then store each additional launch option for use in our program. We can also get even more detailed information, by using the SteamGetAppUserDefinedInfo function defined below: Code: C# -------------------------------------------------------------------------- ------- [DllImport("steam.dll", CallingConvention=CallingConvention.Cdecl)] public static extern int SteamGetAppUserDefinedInfo(uint AppId, string Key, StringBuilder Buff, uint BuffSize, ref uint ResultSize, ref TSteamError SteamErr); VB -------------------------------------------------------------------------- ------- _ Public Function SteamGetAppUserDefinedInfo(ByVal AppId As UInteger, ByVal Key As String, ByVal Buff As System.Text.StringBuilder, ByVal BuffSize As UInteger, ByRef ResultSize As UInteger, ByRef SteamErr As TSteamError) As Integer End Function We call it like so: Code: C# -------------------------------------------------------------------------- ------- TSteamError SErr = new TSteamError(); int iSteamReturn = 0; uint iBuffSize = 512; uint iResultSize = iBuffSize; uint AppID = 70; StringBuilder statebuff = new StringBuilder((int)iBuffSize, (int)iBuffSize); iSteamReturn = SteamGetAppUserDefinedInfo(AppID, "state", statebuff, iBuffSize, ref iResultSize, ref SErr); VB -------------------------------------------------------------------------- ------- Dim SErr As New TSteamError() Dim iSteamReturn As Integer = 0 Dim iBuffSize As UInteger = 512 Dim iResultSize As UInteger = iBuffSize Dim AppID As UInteger = 70 Dim statebuff As New StringBuilder(CInt(iBuffSize), CInt(iBuffSize)) iSteamReturn = SteamGetAppUserDefinedInfo(AppID, "state", statebuff, iBuffSize, iResultSize, SErr) This will return the state of the given AppID, we can then test this to see if, for example, it is "eStateUnavailable" and not to be used, or "eStateTool" – a Steam tool. This test should *not* be case sensitive, as the return result in some instances has been found to be inconsistent Other things we can recover in this way include: "icon" – returns the icon name of the given AppID "MediaFileType" - returns the files media type e.g. if it is a "Movie" "Homepage" – returns the homepage URL for the AppID "GameManualURL" – returns a URL for the game Manual "Developer" – returns a URL for the developer "DemoOfAppID" – Identifies a demo App. Though you should note that not all Steam content has all of these options. Another useful function is SteamGetAppDir as declared below: Code: C# -------------------------------------------------------------------------- ------- [DllImport("steam.dll", CallingConvention=CallingConvention.Cdecl)] public static extern int SteamGetAppDir(uint AppId, StringBuilder Directory, ref TSteamError SteamErr); VB -------------------------------------------------------------------------- ------- _ Public Function SteamGetAppDir(ByVal AppId As UInteger, ByVal Directory As System.Text.StringBuilder, ByRef SteamErr As TSteamError) As Integer End Function This can be used to return the full path of the App directory, or the directory where minimum footprint files are extracted, or that resides in the “common” folder. This always returns a string relevant to: Your SteamApps Path Your current SteamUser So if my SteamApps folder was: C:\Games\Steam\SteamApps And the AppID in question was 70 (Half-Life) and I was logged in as the user “SteamUSER” then this would return: “C:\Games\Steam\SteamApps\ SteamUSER \Half-Life\” We can call it like so: Code: C# -------------------------------------------------------------------------- ------- TSteamError SErr = new TSteamError(); int iSteamReturn = 0; uint AppID = 70; uint iBuffSize = 512; StringBuilder strAppDir = new StringBuilder((int)iBuffSize); iSteamReturn = SteamGetAppDir(AppID, strAppDir, ref SErr); VB -------------------------------------------------------------------------- ------- Dim SErr As New TSteamError() Dim iSteamReturn As Integer = 0 Dim AppID As UInteger = 70 Dim iBuffSize As UInteger = 512 Dim strAppDir As New StringBuilder(CInt(iBuffSize)) iSteamReturn = SteamGetAppDir(AppID, strAppDir, SErr) SteamEmu specific functions hCUPa, being the kind sole that he is, provided developers with a number of SteamEmu specific functions to make some of the more complex tasks we face a lot easier. Note, that SteamEmu functions do not make use of the Steam Error object. These are: Code: C# -------------------------------------------------------------------------- ------- [DllImport("steam.dll", CallingConvention=CallingConvention.Cdecl)] public static extern int SteamEmu_GetGcfList(uint AppID, StringBuilder Buf, uint BufSize); [DllImport("steam.dll", CallingConvention=CallingConvention.Cdecl)] public static extern int SteamEmu_GetWholeGcfCopy(string GCF, string Dest); [DllImport("steam.dll", CallingConvention=CallingConvention.Cdecl)] public static extern int SteamEmu_GetLocalGcfCopy(string GCF, string Dest); [DllImport("steam.dll", CallingConvention=CallingConvention.Cdecl)] public static extern int SteamEmu_GetProperty(string name, StringBuilder Buf, uint BufSize); VB -------------------------------------------------------------------------- ------- _ Public Function SteamEmu_GetGcfList(ByVal appid As UInteger, ByVal Buf As System.Text.StringBuilder, ByVal BufSize As UInteger) As Integer End Function _ Public Function SteamEmu_GetWholeGcfCopy(ByVal GCF As String, ByVal Dest As String) As Integer End Function _ Public Function SteamEmu_GetLocalGcfCopy(ByVal GCF As String, ByVal Dest As String) As Integer End Function _ Public Function SteamEmu_GetProperty(ByVal name As String, ByVal Buf As StringBuilder, ByVal BufSize As UInteger) As Integer End Function SteamEmu_GetGcfList will return a string detailing all of the GCFs required for the AppID specified. The string is delimited by semi colons and has a special use in that files proceeded with a “@” symbol indicate that they are optional and therefore not required to launch the content. We can call it like so: Code: C# -------------------------------------------------------------------------- ------- int iSteamReturn = 0; uint AppID = 70; uint iBuffSize = 512; StringBuilder GCFList = new StringBuilder((int)iBuffSize); iSteamReturn = SteamEmu_GetGcfList(AppID, GCFList, iBuffSize); VB -------------------------------------------------------------------------- ------- Dim iSteamReturn As Integer = 0 Dim AppID As UInteger = 70 Dim iBuffSize As UInteger = 512 Dim GCFList As New StringBuilder(CInt(iBuffSize)) iSteamReturn = SteamEmu_GetGcfList(AppID, GCFList, iBuffSize) And we could process the resulting string like so: Code: C# -------------------------------------------------------------------------- ------- string[] SEGCFList = GCFList.ToString().Split(";".ToCharArray()); foreach (string sgcf in SEGCFList) { if (sgcf.IndexOf("@") == -1 && sgcf != "") { //Mandatory file! } else { //Optional! } } VB -------------------------------------------------------------------------- ------- Dim SEGCFList As String() = GCFList.ToString().Split(";".ToCharArray()) For Each sgcf As String In SEGCFList If sgcf.IndexOf("@") = -1 AndAlso sgcf <> "" Then 'Mandatory file! Else 'Optional! End If Next One quirk of SteamEmu_GetGcfList is that it will return an “ncf” file as the extension “gcf”, so you need to look out for that. SteamEmu_GetLocalGcfCopy and SteamEmu_GetWholeGcfCopy do basically the same thing, except that SteamEmu_GetLocalGcfCopy extracts the required minimum footprint files only, whereas SteamEmu_GetWholeGcfCopy extracts all files from a GCF file. Call them like so: Code: C# -------------------------------------------------------------------------- ------- int iSteamReturn = 0; string sGCF = "c:\\gcf\\winui.gcf"; string sPath = "c:\\SteamExtracted\\"; iSteamReturn = SteamEmu_GetLocalGcfCopy(sGCF, sPath); VB -------------------------------------------------------------------------- ------- Dim iSteamReturn As Integer = 0 Dim sGCF As String = "c:\gcf\winui.gcf" Dim sPath As String = "c:\SteamExtracted\" iSteamReturn = SteamEmu_GetLocalGcfCopy(sGCF, sPath) SteamEmu_GetProperty is used to retrieve SteamEmu configuration settings, you can call it like so: Code: C# -------------------------------------------------------------------------- ------- uint iBuffSize = 512; string PropType = "gcf_root"; StringBuilder buff = new StringBuilder((int)iBuffSize); iSteamReturn = SteamEmu_GetProperty(PropType, buff, iBuffSize); VB -------------------------------------------------------------------------- ------- Dim iBuffSize As UInteger = 512 Dim PropType As String = "gcf_root" Dim buff As New StringBuilder(CInt(iBuffSize)) iSteamReturn = SteamEmu_GetProperty(PropType, buff, iBuffSize) The example above will show the path that SteamEmu is currently using to search for GCF files. There is one more thing worth mentioning about SteamEmu, and that is its ability to pre-load GCFs defined in an environment variable. This is useful for supporting multi-language content or optional GCF files. Below is an example of how to do this: Code: C# -------------------------------------------------------------------------- ------- string AddGCFList = "half-life high definition.gcf"; Environment.SetEnvironmentVariable("SteamEmuPremount", AddGCFList); VB -------------------------------------------------------------------------- ------- Dim AddGCFList As String = "half-life high definition.gcf" Environment.SetEnvironmentVariable("SteamEmuPremount", AddGCFList) You would typically use that code prior to SteamLaunchApp, you should note that you can also define multiple premounts by using a semi-colon seperated string. Well, that's it for now, please play with all the above, and see what other options and information you can get out of Steam. Expect more Steam API related tutorials in the future.... EDIT LOG ====================================== 07/SEP/07 Sectionalised the text for easier reference prior to API tutorial post 2 going live 15/AUG/07 Added cdecl to all function declarations - VS2005 default is stdcall if unspecified! This is not usually a problem with the calls above, as both calling conventions are right to left and support Varargs (in differing degrees), but it may cause problems with advanced callbacks later and is the correct way to do it. 08/AUG/07 Added information about SteamGetAppUserDefinedInfo returning different case on strings in some situations. thanks g3cka 02/AUG/07 Added information about SteamEmu_GetProperty 01/AUG/07 Added VB code 01/AUG/07 First publication Last edited by Mitsukarina on Friday 07th September 2007 05:39 pm; edited 33 times in total Back to top View user's profile Send private message Mitsukarina Super flooder Почетный графоман Joined: 14 Apr 2006 Posts: 599 New postPosted: Wednesday 01st August 2007 05:50 pm Post subject: Reply with quote Steam API revealed - Part Two Steam subscription functions We now know how to process all the applications that are contained in the clientregistry.blob file, so let's look at how we can do the same thing with subscriptions. You can think of a subscription as a collection of applications, we query them in the same way that we queried the applications, using very similar methods to retrieve information about the subscription itself along with a list of the associated AppIDs. Once we have this information, we can then retrieve basic information from the subscription record, and enumerate the applications as we did in the first part of this tutorial. There is a distinct difference between Steam and SteamEmu when it comes to subscriptions, and that is that there is only one subscription that SteamEmu will ever return. This is Subscription 104 - hCUPa's Ultimate Subscription - That is purely a collection of every AppID that is available in the clientregistry.blob file. Subscriptions are therefore largely irelevant if you only want to write a SteamEmu powered launcher, but if we want to write software that can process legitimate Steam accounts, then we need to understand the subscription calls a little more. The structures that we require are as follows: Code: C# -------------------------------------------------------------------------- ------- [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 4)] public struct TSteamSubscriptionStats { public uint NumSubscriptions; public uint MaxNameChars; public uint MaxApps; } [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 4)] public struct TSteamSubscription { public string Name; public uint MaxNameChars; public IntPtr AppIDs; public uint MaxAppIDs; public uint ID; public uint NumApps; public uint DurationInSeconds; public uint CostInCents; public uint IsAutoRenewing; } VB -------------------------------------------------------------------------- ------- _ Public Structure TSteamSubscriptionStats Public NumSubscriptions As UInteger Public MaxNameChars As UInteger Public MaxApps As UInteger End Structure _ Public Structure TSteamSubscription Public Name As String Public MaxNameChars As UInteger Public AppIDs As IntPtr Public MaxAppIDs As UInteger Public ID As UInteger Public NumApps As UInteger Public DurationInSeconds As UInteger Public CostInCents As UInteger Public IsAutoReNewing As UInteger End Structure The main thing to note about the structures above is that we need to use a pointer to reference the AppID array for the subscription in the structure TSteamSubscription. There are a number of ways of passing a structure to unmanaged code that contains a pointer to an array, the code below illustrates doing this using manual marshaling of the array memory, which is probably the simplest method to use. The functions that we will need to use for subscriptions are: Code: C# -------------------------------------------------------------------------- ------- [DllImport("steam.dll", CallingConvention = CallingConvention.Cdecl)] public static extern int SteamGetSubscriptionStats(ref TSteamSubscriptionStats SubscriptionStats, ref TSteamError SteamErr); [DllImport("steam.dll", CallingConvention = CallingConvention.Cdecl)] public static extern int SteamGetSubscriptionIds(uint[] SubscriptionIDs, uint MaxSubscriptionIDs, ref TSteamError SteamErr ); [DllImport("steam.dll", CallingConvention = CallingConvention.Cdecl)] public static extern int SteamEnumerateSubscription(uint SubscriptionID, ref TSteamSubscription Subscription, ref TSteamError SteamErr); [DllImport("steam.dll", CallingConvention = CallingConvention.Cdecl)] public static extern int SteamIsSubscribed(uint SubscriptionId, ref uint IsSubscribed, ref uint IsSubscriptionReady, ref TSteamError SteamErr); VB -------------------------------------------------------------------------- ------- _ Public Function SteamGetSubscriptionStats(ByRef SubscriptionStats As TSteamSubscriptionStats, ByRef SteamErr As TSteamError) As Integer End Function _ Public Function SteamGetSubscriptionIds(ByVal SubscriptionIDs As UInteger(), ByVal MaxSubscriptionIDs As UInteger, ByRef SteamErr As TSteamError) As Integer End Function _ Public Function SteamEnumerateSubscription(ByVal SubscriptionID As UInteger, ByRef Subscription As TSteamSubscription, ByRef SteamErr As TSteamError) As Integer End Function _ Public Function SteamIsSubscribed(ByVal SubscriptionId As UInteger, ByRef IsSubscribed As UInteger, ByRef IsSubscriptionReady As UInteger, ByRef SteamErr As TSteamError) As Integer End Function These functions are the "subscription" equilavent of the application functions that we have already explored. The code to process the subscriptions is very similar to the application enumeration code in the first part of this tutorial, the only difference is in the marshaling of the TSteamSubscription structures AppID array. So, assuming we have logged in to Steam: Code: C# -------------------------------------------------------------------------- ------- uint iBuffSize = 512; int iSteamReturn = 0; TSteamAppStats AppStats = new TSteamAppStats(); iSteamReturn = SteamGetAppStats(ref AppStats, ref SErr); TSteamSubscriptionStats SubscriptionStats = new TSteamSubscriptionStats(); iSteamReturn = SteamGetSubscriptionStats(ref SubscriptionStats, ref SErr); if (SubscriptionStats.NumSubscriptions > 0) { uint[] RawSubscriptionIDs; RawSubscriptionIDs = new uint[SubscriptionStats.NumSubscriptions]; iSteamReturn = SteamGetSubscriptionIds(RawSubscriptionIDs, SubscriptionStats.NumSubscriptions, ref SErr); uint[] tAppIDs; tAppIDs = new uint[AppStats.NumApps]; //set the array to the maximum amount of Steam Apps // Here we are creating a pointer to the tAppIDs array and allocating the correct amount of memory for manual marshaling object arraytype = tAppIDs.GetValue(0); int arraysize = Marshal.SizeOf(arraytype); int buffer = arraysize * tAppIDs.Length; IntPtr intPtr = Marshal.AllocHGlobal(buffer); uint isSubSubscribed = 0; uint isSubReady = 0; foreach (uint RawSubscriptionID in RawSubscriptionIDs) { iSteamReturn = SteamIsSubscribed(RawSubscriptionID, ref isSubSubscribed, ref isSubReady, ref SErr); if (isSubSubscribed == 1) { TSteamSubscription SubNFO = new TSteamSubscription(); SubNFO.Name = new String(' ', 255); SubNFO.MaxNameChars = 255; SubNFO.AppIDs = intPtr; SubNFO.MaxAppIDs = AppStats.NumApps; iSteamReturn = SteamEnumerateSubscription(RawSubscriptionID, ref SubNFO, ref SErr); int ofs = 0; uint uLoop; for (uLoop = 0; uLoop <= SubNFO.NumApps - 1; uLoop++) { //populate our app array from marshaled memory tAppIDs[uLoop] = (uint)Marshal.ReadInt32(intPtr, ofs); ofs += arraysize; } //Do Something! for (uLoop = 0; uLoop < SubNFO.NumApps; uLoop++) //Clear the relevant array indexes for the next subscription { tAppIDs[uLoop] = 0; } } } Marshal.FreeHGlobal(intPtr); //free the marshaled memory } VB -------------------------------------------------------------------------- ------- Dim iBuffSize As UInteger = 512 Dim iSteamReturn As Integer = 0 Dim AppStats As New TSteamAppStats() iSteamReturn = SteamGetAppStats(AppStats, SErr) Dim SubscriptionStats As New TSteamSubscriptionStats() iSteamReturn = SteamGetSubscriptionStats(SubscriptionStats, SErr) If SubscriptionStats.NumSubscriptions > 0 Then Dim RawSubscriptionIDs As UInteger() RawSubscriptionIDs = New UInteger(SubscriptionStats.NumSubscriptions - 1) {} iSteamReturn = SteamGetSubscriptionIds(RawSubscriptionIDs, SubscriptionStats.NumSubscriptions, SErr) Dim tAppIDs = New UInteger(AppStats.NumApps) {} 'set the array to the maximum amount of Steam(Apps) ' Here we are creating a pointer to the tAppIDs array and allocating the correct amount of memory for manual marshaling Dim arraytype As Object = tAppIDs.GetValue(0) Dim arraysize As Integer = Marshal.SizeOf(arraytype) Dim buffer As Integer = arraysize * tAppIDs.Length Dim intPtr As IntPtr = Marshal.AllocHGlobal(buffer) Dim isSubSubscribed As UInteger = 0 Dim isSubReady As UInteger = 0 For Each RawSubscriptionID As UInteger In RawSubscriptionIDs iSteamReturn = SteamIsSubscribed(RawSubscriptionID, isSubSubscribed, isSubReady, SErr) If isSubSubscribed = 1 Then Dim SubNFO As New TSteamSubscription() SubNFO.Name = New String(" "C, 255) SubNFO.MaxNameChars = 255 SubNFO.AppIDs = intPtr SubNFO.MaxAppIDs = AppStats.NumApps iSteamReturn = SteamEnumerateSubscription(RawSubscriptionID, SubNFO, SErr) Dim ofs As Integer = 0 Dim uLoop As Integer For uLoop = 0 To SubNFO.NumApps - 1 'populate our app array from marshaled memory tAppIDs(uLoop) = Marshal.ReadInt32(intPtr, ofs) ofs = ofs + arraysize Next 'Do Something! For uLoop = 0 To SubNFO.NumApps - 1 'Clear the relevant array indexes for the next subscription tAppIDs(uLoop) = 0 Next End If Next Marshal.FreeHGlobal(intPtr) 'free the marshaled memory End If The code above gives us the subscription record with its relevant information, along with the subscriptions associated application IDs. The more advanced among you may want to explore the following alternative for C# that allows the easier creation of a pinned reference to the array using an unsafe code block. This method is not supported at all in VB.NET, but is simpler to use in C# as it removes the need to manually copy the array values from unmanaged memory into our array. Code: C# -------------------------------------------------------------------------- ------- IntPtr intPtr; unsafe { fixed (uint* pArray = tAppIDs) { intPtr = new IntPtr((void*)pArray); } } Please feel free to play with this code, until the next update. EDIT LOG ====================================== 09/SEP/07 First publication Last edited by Mitsukarina on Sunday 09th September 2007 04:29 pm; edited 3 times in total