Friday 25 April 2014

Writing a StrPix-like program with Lazarus – 5

To add IceBerg’s code for reading a *.wad file to our program, with our project open in Lazarus, go to the File menu and select Open.

In the Open File dialog, navigate to the WadExplorer source code folder (WADE11sources) we copied to our Lazarus projects folder.

Select and open TombRaiderWad.pas which contains all the code necessary to read a *.wad file.

IceBergsWadUnit
TombRaiderWad.pas should now be the file displayed in the source editor.

Go to the Project menu and select Add Editor File to Project and press Yes to confirm.
Press OK in the next dialog to add the WadExplorer source code folder to the project’s search path.
Press the Run button to build and run our program. This will compile IceBerg’s code.
Check the Lazarus Messages window for warnings and errors.

CompilerMessages

Thankfully IceBerg’s Delphi code compiles without any errors. IceBerg didn’t use any Delphi code in this unit that is incompatible with FreePascal.

If there were errors we could let the FreePascal compiler know the code is Delphi code by putting the directive {$mode Delphi} at the top of the TombRaiderWAD unit. This, however, is still no guarantee that Delphi code will compile without errors.

We need to study IceBerg’s code to see how to use it in our program.

Once you have studied FreePascal for a while and done plenty of beginner tutorials you should be able to understand the code but for this exercise I’ll just outline the main points.
  • The code expects a *.wad file to already be loaded into memory
  • The contents of the *.wad file are stored in a variable named Wad
  • The variable Wad is declared in the interface part of the unit so can be used by code in other units.
The interface section of a unit is the top section and is indicated in the Lazarus Source Editor by a darker grey section in the gutter next to the vertical scrollbar.

interface

The code extracts the *.wad data in two passes. In the first pass it finds the position in the file of the various data sections of the *.wad and stores the positions in the Wad variable.

In the second pass it jumps to each data section of the file and extracts the data.

We also need to modify some code.

With the TombRaiderWAD unit displayed in the source editor, go to the Search menu, select Goto Line and input 302.

This takes us to a function that checks the version number of a *.wad file. A valid *.wad can now also have a version number of 130 so we need to change line 302 from

Result := ( wad.version = 129 );

to

Result := ( wad.version = 129 ) or ( wad.version = 130 );

Press the Run button to build and run the code to test that we still get no errors. Running the program also saves the source code files and project files.

To see how IceBerg uses the code we need to examine WadExplorer’s main unit.

Go to the File menu and select Open. Navigate to the WadExplorer source code folder and select and open MainUnit.pas. Do not add this file to the project. We have only opened it in Lazarus to examine the code.

To find where in MainUnit.pas the *.wad file is opened, think about how you open a file in WadExplorer. You right click, then click Load Wad on a popup menu and an Open File dialog appears. So we might find the code we want by searching for the word “dialog”.

In TombRaiderWad.pas there is a procedure called ExtractAll so we could also search for “extractall”.

Go to the Search menu and select Find, then search for “dialog”.

After a few Find Nexts we find the code we want.

//============================================================================//
//=== LOAD THE WAD FILE ======================================================//
//============================================================================//
procedure TMain.LoadWadClick(Sender: TObject);
var
err:string;
valid:Integer;
begin
    // Processing all windows messages before and after using the
    // dialog box keeps the user interface clean.
    Application.ProcessMessages;
    if not Main.OpenDialogWad.Execute then exit;
    Application.ProcessMessages;
    // Make sure that wads will not accumulate in memory.
    // Free is successful even if the memory stream was not yet created.
    streamWAD.Free;
    streamWAD := TMemoryStream.Create;
    streamWAD.LoadFromFile( Main.OpenDialogWad.FileName );
    Main.TextBox.Caption := Main.OpenDialogWad.FileName;
    // Crawl through the file for validity and find the main sections.
    valid := WADCrawler.IsValidWad( streamWAD, err );
    if valid < 0 then
    begin
        MessageDlg( 'File is not a valid WAD.' + #10 + err,
                    mtWarning,
                    [mbOK],
                    0 );
        exit;
    end
    else if valid > 0 then
        MessageDlg( 'File is not a regular WAD.' + #10 + err,
                    mtWarning,
                    [mbOK],
                    0 );
    // The main sections were already detected by the crawler.
    // Now parse the WAD file and extract the data tables.
    WADParser.ExtractAll( streamWAD );
    // Build the Wad Tree.
    Main.MakeWadTree;
    Main.TreeViewWad.FullExpand;
    // Prepare the user data interface.
    Main.PrepareWadDataForDisplay;
    // Enable the other file options in the popup.
    //Main.SaveWad.Enabled   := TRUE;
    //Main.SaveAsWad.Enabled := TRUE;
end;

The code after double slashes (//) is a comment. These lines are ignored and not compiled so you are free to write anything here and use any symbols.

OpenDialogWad.Execute is the command that makes the Open File dialog show on screen. It is usually found in an If statement because if the user clicks the Cancel button you want to perform different code than if the Open button was clicked.

We use an Action to display our Open File dialog and use different names so we can’t just copy and paste the whole procedure into our program and use it as is but we can copy the code and paste it and then modify it where required.

So select the whole procedure, right click and click Copy.

Go to the Window menu and click on Form1 to display our form.

Double click on the ActionList1 control and select FileOpen1 in the Action list to display the FileOpen1 action’s properties in the Object Inspector.

Click on the Events tab and then OnAccept in the list of events.

FileOpen1onAccept

A dropdown and button should appear next to OnAccept.
Click on the button with the three dots (ellipsis) and Lazarus will create a procedure in unit1’s code and jump to it.

This procedure is the code that will be performed when a user clicks on the Open button in the Open File dialog that appears when the Open menu is clicked or the shortcut ctrl+o is pressed.

At the moment nothing will happen because the procedure is empty. There is no code or comments between “begin” and “end”.
Build an run the program to test that this is the case if you wish.

OnAcceptMethodAdded

If you wish to remove an entire procedure from a unit, the way to do it is to make it an empty procedure (or function) and place the cursor inside that empty procedure’s “begin” and “end”.

Then right click and select Refactoring>Empty Methods. A list should appear with all empty methods in the unit which you can then remove by clicking the Remove Methods button.
Method is another name for procedure or function.

You should not remove an unwanted procedure by using text editor delete or cut options since you must also remove the declarations which appear at the top of the unit. Using the refactoring option removes the declarations automatically.

RemoveEmptyMethod


Now paste the code we copied earlier into the procedure we created. If you try to build the program there will be many errors since we pasted variable names (or identifiers) our program doesn’t know about and also have incorrect syntax but we will modify the code to make the code compile.

This is what the TForm1FileOpen1Accept procedure looks like.

procedure TForm1.FileOpen1Accept(Sender: TObject);
begin
  procedure TMain.LoadWadClick(Sender: TObject);
  var
  err:string;
  valid:Integer;
  begin
      // Processing all windows messages before and after using the
      // dialog box keeps the user interface clean.
      Application.ProcessMessages;
      if not Main.OpenDialogWad.Execute then exit;
      Application.ProcessMessages;
      // Make sure that wads will not accumulate in memory.
      // Free is successful even if the memory stream was not yet created.
      streamWAD.Free;
      streamWAD := TMemoryStream.Create;
      streamWAD.LoadFromFile( Main.OpenDialogWad.FileName );
      Main.TextBox.Caption := Main.OpenDialogWad.FileName;
      // Crawl through the file for validity and find the main sections.
      valid := WADCrawler.IsValidWad( streamWAD, err );
      if valid < 0 then
      begin
          MessageDlg( 'File is not a valid WAD.' + #10 + err,
                      mtWarning,
                      [mbOK],
                      0 );
          exit;
      end
      else if valid > 0 then
          MessageDlg( 'File is not a regular WAD.' + #10 + err,
                      mtWarning,
                      [mbOK],
                      0 );
      // The main sections were already detected by the crawler.
      // Now parse the WAD file and extract the data tables.
      WADParser.ExtractAll( streamWAD );
      // Build the Wad Tree.
      Main.MakeWadTree;
      Main.TreeViewWad.FullExpand;
      // Prepare the user data interface.
      Main.PrepareWadDataForDisplay;
      // Enable the other file options in the popup.
      //Main.SaveWad.Enabled   := TRUE;
      //Main.SaveAsWad.Enabled := TRUE;
  end;
end;

Make the changes as below. Note that some lines have been deleted whereas others have just been commented with lots of slashes at the start.
Sometimes it is better to comment a line you don’t want the compiler to see rather than delete it because sometimes you may want to use the commented line again or use it for reference.

procedure TForm1.FileOpen1Accept(Sender: TObject);
var
  err: string;
  valid: integer;
begin
  // Processing all windows messages before and after using the
  // dialog box keeps the user interface clean.
  Application.ProcessMessages;
  ////////////// if not Main.OpenDialogWad.Execute then
  //////////////  exit;
  Application.ProcessMessages;
  // Make sure that wads will not accumulate in memory.
  // Free is successful even if the memory stream was not yet created.
  streamWAD.Free;
  streamWAD := TMemoryStream.Create;
  streamWAD.LoadFromFile(Main.OpenDialogWad.FileName);
  ////////////// Main.TextBox.Caption := Main.OpenDialogWad.FileName;
  // Crawl through the file for validity and find the main sections.
  valid := WADCrawler.IsValidWad(streamWAD, err);
  if valid < 0 then
  begin
    MessageDlg('File is not a valid WAD.' + #10 + err,
      mtWarning,
      [mbOK],
      0);
    exit;
  end
  else if valid > 0 then
    MessageDlg('File is not a regular WAD.' + #10 + err,
      mtWarning,
      [mbOK],
      0);
  // The main sections were already detected by the crawler.
  // Now parse the WAD file and extract the data tables.
  WADParser.ExtractAll(streamWAD);
  // Build the Wad Tree.
  //////////////Main.MakeWadTree;
  //////////////Main.TreeViewWad.FullExpand;
  // Prepare the user data interface.
  //////////////Main.PrepareWadDataForDisplay;
  // Enable the other file options in the popup.
  //Main.SaveWad.Enabled   := TRUE;
  //Main.SaveAsWad.Enabled := TRUE;
end;

The indentation of the pasted code doesn’t match existing code so I went to the Source menu and selected JEDI code format>Current editor window and formatted the code. Note if you have syntax errors in the code the JEDI formatter does not work.

We still have to add some code so that it will compile.
If you try to build the program there will be many identifier unknown errors in the Messages window.

Our program does not know what streamWAD, WADCrawler or WADParser are because they are not defined in unit1, our main unit.

By studying WadExplorer’s MainUnit we see that IceBerg declares these as global variables in the implementation section so we will do the same.

A global variable is one that is not declared inside a function or procedure.

Add these lines to unit1.

…
implementation

{$R *.lfm}

var //NEW
  streamWAD   : TMemoryStream; //NEW
  WADCrawler  : TWADCrawler; //NEW
  WADParser   : TWADParser; //NEW

{ TForm1 }
…

The TWADCrawler and TWADParser types are defined in the TombRaiderWAD unit so for unit1 to know about them we must add TombRaiderWAD to a “uses” clause of unit1. Unit1 also does not know what the Wad variable is until we add TombRaideWAD to a "uses" clause.

There are two “uses” clauses in a unit. One in the interface section and one in the implementation section.

At the moment none of the types or variables or method declarations in the interface section of unit1 need to know what a TWADCrawler or TWADParser type is so we will add TombRaiderWAD to the implementation “uses” clause. We need to add the word “uses” too since this is the first unit we use.

…
implementation

uses TombRaiderWAD; //NEW

{$R *.lfm}

var
  streamWAD   : TMemoryStream;
  WADCrawler  : TWADCrawler;
  WADParser   : TWADParser;  

{ TForm1 }
…


Now our main unit, unit1, knows where to find the definitions of TWADCrawler and TWADParser.

Our program knows what streamWAD, WADCrawler and WADParser are but we need to make one more change to make the code compile successfully.

In the procedure TForm1.FileOpen1Accept the line

streamWAD.LoadFromFile(Main.OpenDialogWad.FileName);

must be changed to

streamWAD.LoadFromFile(FileOpen1.Dialog.FileName); 

because our program does not know what Main.OpenDialogWad is. This is the name of the OpenDialog control of WadExplorer’s form named Main.

We do not have an OpenDialog control on our form. Our Open File dialog is built into the action FileOpen1 so the name of our dialog is FileOpen1.OpenDialog. We do not need to prefix the form name as Iceberg did since unit1 will assume Form1.

Build and run the program and open any file.

If it is not a valid *.wad file the program should display an error message and exit the procedure without extracting the data.

If it is a valid *.wad file but has a different format than defined in the TombRaiderWAD unit the program will display a message about not being a regular *.wad but still extract the data.

If it is a valid and regular *.wad no message should be displayed.

When you close the program you will get an error notification from Heaptrc that there were unfreed memory blocks.

Basically any variable that is created with SomeClassType.Create must be freed using VariableName.Free or FreeAndNil(VariableName).

When we open a file, our program frees the memory used by the variable streamWAD and then gets some new memory for the variable streamWAD using TMemoryStream.Create.

Then the program transfers the file on disk into the memory reserved by streamWAD by the method streamWAD.LoadFromFile.

When we close the program though, the file we opened still exists in memory and this is why we get the unfreed memory error.

So we need to add code to the program to free the memory used by the variable streamWAD on program exit.

In Lazarus, in the Object Inspector window, select our form, Form1.
This displays the properties for the form.
Select the Events tab and scroll through the list and click on OnClose.
Click on the button that appears to add a FormClose procedure to the program.

OnClose

The code in this procedure will be performed whenever the Form is closed. Remember the form is the program’s window so this code will be performed whenever we exit the program.

We want to free the memory used by streamWAD here since we will no longer need the variable if the user is exiting the program.

Add code to the FormClose procedure as shown below.

procedure TForm1.FormClose(Sender: TObject; var CloseAction: TCloseAction);
begin
  streamWAD.Free;
end; 

You may notice that as you type in "streamWAD.Free", when you type the dot and wait, Lazarus will popup a list of methods and properties that are valid. You can select the method you want from this list if you wish.

Note that if this popup does not appear you may have an error in your code somewhere. Error’s prevent Lazarus’s editor from displaying code hints like this.

Build and run and save the program by clicking the Run button.

When we close the program we have still have unfreed memory blocks. You must open at least one regular *.wad file to get this unfreed memory error.

In unit1 we only construct the streamWAD variable with a Create method so there must be a variable or variables in the TombRaiderWAD unit that are constructed with a Create method.

Looking at the TWAD type we see that the variable textureMap is of the type TBitmap. Variables of the TBitmap type are constructed by the TBitmap.Create method so maybe this is the cause of the unfreed memory. Looking through the code we see that Wad.textureMap is only freed when a valid *.wad file is opened.

We could free Wad.textureMap in our form's FormClose method the same as we did for streamWAD and it would work fine. But freeing Wad.textureMap doesn't really belong in our main unit. The Wad variable is created in the TombRaiderWAD unit so that's where it should be freed.

With Wad.textureMap freed in the TombRaiderWAD unit, if we use that unit in another program we don't have to remember to free Wad.textureMap in the main unit of that program.

The TombRaiderWAD unit does not have a form so we can't put it in a FormClose method so where to write the code?

The answer is the unit's finalization section. Everything I know about the finalization section I read from Help by clicking on the word finalization in my code to position the cursor and then pressing F1.

Help says that when a program closes, the code in the finalization section of a unit is executed. The finalization section is the very last section in a unit between the word "finalization" and "end."

So here is the code I added to the bottom of the TombRaiderWAD unit.

procedure TWadParser.ExtractAll( memwad: TMemoryStream );
begin
    self.ExtractTextureTable ( memwad );
    self.ExtractTextureMap   ( memwad );
    //
    self.ExtractMovablesTable( memwad );
    self.ExtractStaticsTable ( memwad );
    //
    self.ExtractMeshPointers ( memwad );
    self.ExtractAnimTable    ( memwad );
    self.ExtractStatesTable  ( memwad );
    self.ExtractDispatsTable ( memwad );
end;

//============================================================================//
//============================================================================//
//============================================================================//

finalization  //NEW
  Wad.textureMap.Free; //NEW
end.             

Build and run the program and now if a *.wad is opened we should have 0 unfreed memory blocks.

Now we have a program that opens a *.wad file and reads and stores the information in memory.

Next time we will add controls to display some information from the *.wad file.


prev | next

No comments:

Post a Comment