Stone's Widescreen Patcher Developer Guide Part 1 - INI Files

Submitted by stone on 1 February, 2012 - 15:18

This is first part of Developer Guide. In this part I focus on INI file modification.
As example I took an INI file of the game Homefront.

screenshot

1. Structure and basic patch data

Since this is a very first tutorial, let me introduce XML patch concept.
Spend a moment to familiarize with XML structure...

<?xml version="1.0" encoding="UTF-8"?>
<wsp>
  <name>Homefront</name>
  <version>1.1</version>
  <author>Stone</author>
  <link>http://widescreengamingforum.com/dr/homefront</link>
  <description>This patch does the same thing as my Homefront Launcher</description>
  <conditions>
    ...
  </conditions>
  <patch id="0A173D61-5FD5-44F0-A846-784063FAA40F">
    <patchname>Homefront INI File</patchname>
    <description>this patch modifies GCEngine.ini file</description>
    <conditions>
      ...
    </conditions>
    <action>
      <type>ini_file</type>
      <conditions>
        ...
      </conditions>
      <description>This action changes Homefront resolution in INI file according to user input.</description>
      <ini_file>result := GetSpecialFolder(CSIDL_PERSONAL) + '\My Games\HOMEFRONT\GCGame\Config\GCEngine.ini';</ini_file>
      <gui>
        <component>
          ...
        </component>
      </gui>
      <params>
        <p>
          ...
        </p>
      </params>
    </action>
  </patch>
</wsp>

At top of the structure there are some obvious tags:
  a. <name> - name of the game
  b. <version> - version of the patch
  c. <author> - creator of the patch
  d. <link> - jump to game's DR
  e. <description> - auxiliary game info

Other tags definitions:
  <patch> - this is complete game patching solution
  <action> - single action of patching e.g. file, registry
  <conditions> - must be fulfilled before patching may be executed
  <gui> - user interface

Remaining tags will be explain later together with parent tags.

2. Patch

You can imagine it may be necessary to have separate patching solutions for Windows XP and Windows 7. Same game, different patching scenarios. That's why you can place several <patch> sections in one XML file. For most games there will be only one such section, but some may have more. A user will execute only the patch, which fits to his environment.
The tag <patch> may contain one or more <action> sections. See next chapter.

3. Action

Action refers to operation on a single object like a file or registry. Since patching may require several different objects to be changed (file, registry) it is very convenient to encapsulate one object manipulation into one action. Again, most games will have only one <action>, but some may have more.
Action contains a tag <type> which defines what kind of operation will be performed. Available section types: ini_file, text_file, binary_file, registry, memory.
Another section of Action is <gui>. It defines user interface and will be described later.

4. Conditions

Conditions are requirements which a system must fulfill before it can be patched. These are quite obvious: file must exist, registry entry must exist, game must be installed, etc.
You may notice that there are 3 sections of <conditions>: under <wsp>, under <patch> and under <action>. This split is necessary because one XML file can contain several <patch> sections, and every <patch> section can contain several <action> sections. Every level may have its own requirements. Conditions in <patch> or <action> will be rather rare, but there such availibility must exist.

5. GUI (Graphical User Interface)

User interface and scripting is what makes Widescreen Patcher powerful. Have a look at the definition of the single text box component:

    <gui>
      <component>
        <name>tb_screen_xres</name>
        <type>textbox</type>
        <label noscript="true">x resolution</label>
        <!-- ScreenWidth if global variable, available everywhere -->
        <text>result := ScreenWidth;</text>
      </component>
      <component>
        ... next component ...
      </component>
    </gui>

Such approach allows developer to create all sorts of interface controls: lables, text boxes, check boxes, etc.
Tags <type> and <name> require simple text. Other tags may include scripting! e.g. text in text box will be current system screen width thanks to ScreenWidth function. Sure, you need to know functions, but with a nice list which I am going to prepare, scripting will be feasible. No need to say how helpful it can be. You may find the power of scripting in next example:

    <component>
      <type>textbox</type>
      <name>tb_screen_ar</name>
      <label noscript="true">Ratio:</label>
      <readonly>TRUE</readonly>
      <text>result := tb_screen_xres.Text / tb_screen_yres.Text;</text>
    </component>

The aspect ration is calculated from data in text boxes within GUI. The tag <readonly> makes text box non-editable, but it is desired for aspect ration, right? You can ask what if user changes width or heigth? Aspect ration should change automatically as well. In such case you add an event to "tb_screen_xres" component:

    <component>
      <name>tb_screen_xres</name>
        ...
      <on_change>tb_screen_ar.Text := tb_screen_xres.Text / tb_screen_yres.Text;</on_change>
    </component>

I am sure you're freaking out now after understanding how fantastic scripting is :) But wait... there's more:

    <component>
      <type>textbox</type>
      <readonly>TRUE</readonly>
      <name>tb_hf_xres</name>
      <label noscript="true">ResX</label>
      <text>result := GetINIFileParam(GetSpecialFolder(CSIDL_PERSONAL) + '\My Games\HOMEFRONT\GCGame\Config\GCEngine.ini', 'SystemSettings', 'ResX');</text>
    </component>

You have certainly noticed two functions (GetINIFileParam, GetSpecialFolder) and contant (CSIDL_PERSONAL).
The GetINIFileParam is Pascal function and retrieves parameter from INI file:

GetINIFileParam('C:\Users\StoneMy Documents\My Games\HOMEFRONT\GCGame\Config\GCEngine.ini', 'SystemSettings', 'ResX');

where INI file it looks like:

[SystemSettings]
ResY=1920

The second function returns path to My Documents folder
GetSpecialFolder(CSIDL_PERSONAL) equals 'C:\Users\Stone\My Documents'

6. Update of INI File Parameters

You may have noticed that <action> section contains <params> section:

    <params>
      <p>
        ...
      </p>
    </params>

Those tags define changes to be applied to ini file parameters.
Have a look at an example of parameter:

      <p>
        <section noscript="TRUE">SystemSettings</section>
        <param_name noscript="TRUE">ResX</param_name>
        <param_value>result := tb_screen_xres.Text;</param_value>
      </p>

This tells the program to update "ResX" parameter in section "SystemSettings" of INI file.
The value for "ResX" parameter is taken from GUI text box "tb_screen_xres" described above.
[SystemSettings]
...
ResX=3600
...

7. Backup of ini file

Every patch action follows backup operation. INI files are easy to backup.
Before patching a copy of INI file is made with extension .undoUWSP.
For safety reasons re-patching is not possible until backup file exists.

It is possible to prevent creating backup file by placing such tag in Action section:

<no_backup>TRUE</no_backup>

8. More about scripting

Scripting is based on Pascal. The first important rule is returning result. In the example below I discover location of configuration file Mydefault.ini (Advent Rising, http://widescreengamingforum.com/dr/advent-rising) based on UninstallString from registry. Unfortunately this registry parameter UninstallString contains path with setup.exe at the end plus some command line parameters. I cut unnecessary part to get something like "..\Program Files\Advent Rising\System". Finally I merge the path and file name and return it with "result".

    <file>
      _key := 'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Advent';
      _uninstall_string := GetRegistryStringEntry(_key, 'UninstallString');
      if Length(_uninstall_string) > 0 then
      begin
        _position := Pos('uninstall', _uninstall_string);
        _path := Copy(_uninstall_string, 1, _position - 2);
        MydefaultFile := IncludeTrailingPathDelimiter(ExtractFilePath(_path)) + 'Mydefault.ini';
        result := MydefaultFile;
      end;
    </file>

Another important aspect is disabling scripting. You may do that by adding noscript="TRUE". By default scripting is switched on for all relevant tags. Some tags are plain text like action types and component names. Have a look at examples above and find disabled scripting.

9. Events

Events allow to run custom macros in specific moments like opening patch, executing patch or checking condition:

<condition>
 ...
 <on_condition>
  if not RegistryKeyExists('HKEY_CURRENT_USER\Software\Frozen Codebase LLC\AC-130\Video') then
  begin
   RegistryCreateKey('HKEY_CURRENT_USER\Software\Frozen Codebase LLC\AC-130\Video');
   RegistryCreateDWORDValue('HKEY_CURRENT_USER\Software\Frozen Codebase LLC\AC-130\Video', 'Width', ScreenWidth);
   RegistryCreateDWORDValue('HKEY_CURRENT_USER\Software\Frozen Codebase LLC\AC-130\Video', 'Height', ScreenHeight);
   ShowMessage('Registry key has been created. Restart Widescreen patcher.');
  end;
 </on_condition>
</condition>

The following events are possible:
wsg\conditions\condition\on_condition
wsg\patch\conditions\condition\on_condition
wsg\patch\action\conditions\condition\on_condition
wsg\patch\action\gui\component\on_change
wsg\patch\action\events\on_execute
wsg\patch\action\events\on_showaction

10. Available script functions

function GetRegistryStringEntry(_key, _name : string) : string;
function GetRegistryDWORDEntry(_key, _name : string) : string;
function GetEnvironmentVariableValue(const VariableName : string): string;
function GetINIFileParam(file_name, section, param : string) : string;
function BoolToStr(status : boolean) : string;
function StrToBool(s : string) : boolean;
function SearchAndReplace(sSrc, sLookFor, sReplaceWith : string ) : string;
function IntToStr(Value: Integer): string;
function IntToHex(Value: Integer; Digits: Integer): string;
function HexToInt(HexNum: string): LongInt;
function StrToInt(const S: string): Integer;
function Copy(const s: string; index, count: Integer): string;
function CalculateFOV(width, height : integer) : Extended;
function getSpecialFolder(FolderIdx: Integer): String;

Here are constants which can be used in getSpecialFolder function:
CSIDL_DESKTOP = $00;
CSIDL_PROGRAMS = $02;
CSIDL_PERSONAL = $05;
CSIDL_STARTUP = $07;
CSIDL_RECENT = $08;
CSIDL_BITBUCKET = $0A;
CSIDL_STARTMENU = $0B;
CSIDL_MYDOCUMENTS = $0C;
CSIDL_DESKTOPDIRECTORY = $10;
CSIDL_DRIVES = $11;
CSIDL_NETWORK = $12;
CSIDL_COMMON_STARTMENU = $16;
CSIDL_COMMON_PROGRAMS = $17;
CSIDL_COMMON_STARTUP = $18;
CSIDL_COMMON_DESKTOPDIRECTORY = $19;
CSIDL_APPDATA = $1A;
CSIDL_LOCAL_APPDATA = $1C;
CSIDL_COMMON_APPDATA = $23;
CSIDL_WINDOWS = $24;
CSIDL_SYSTEM = $25;
CSIDL_PROGRAM_FILES = $26;
CSIDL_MYPICTURES = $27;
CSIDL_PROFILE = $28;
CSIDL_SYSTEMX86 = $29;
CSIDL_PROGRAM_FILESX86 = $2A;
CSIDL_PROGRAM_FILES_COMMON = $2B;
CSIDL_PROGRAM_FILES_COMMONX86 = $2C;
CSIDL_COMMON_DOCUMENTS = $2E;

11. Certificates

Some files are being created during first execution of the program: master_list.xml, update_list.xml and user_config.xml. Patch files will be downloaded later if you select them in the program. The master list contains information about names of actual game patch xml files and png art files. It also contains a certificate of approval:

<crc>5/06scolj70=</crc>

This code is encrypted check sum of the file. If you create your own patch, you can still attach it to master_list.xml but every time you select this patch, the program will ask for confirmation. This is necessary to increase user's awareness, since macros in patches are very powerful.
After you apply your patch to WSGF for approval, it will be included in official master_list.xml, will get a certificate and will be available for automatic download from server.