Search code examples
pascalpascalscript

Pascal scripting for xEdit - how do I check whether a substring is present in a string?


It seems like this should be straightforward, but maybe it's not. Full disclosure: I am not a programmer by any stretch. I just know enough to muddle my way through hobby projects, and Pascal is brand new for me as of today. So, if anything is just really terrible code, I'm open to improvements. I'm probably only going to use this script once, though, so I'm not terribly concerned about optimization, just reliable operation.

I'm writing a script for xEdit, aka SSEEDit - the tool most people use for mucking around with Skyrim and Fallout 4 mods that don't make changes to rich game assets. The goal is to dump the raw text content of the DESC property for every BOOK object in the game (vanilla + original DLC). I've gotten pretty far and have a decent understanding of what I'm doing, but I cannot for the life of me get this part to work.

I have a blacklist of strings which should be skipped when iterating through all the BOOK objects. I've tried using pos() several different ways, and it never comes back with anything other than zero.

The weird thing is, even if I can get a correct result from pos() in testing, every book is a positive match when I run my actual script. It also doesn't seem like I'm actually iterating past the first entry in the blacklist.

Here's an example of what's returned when I search for a blacklist entry:

EDID: DLC2dunFahlbtharzJournal02
looking in DLC2dunFahlbtharzJournal02
     for DLC1ElderScroll...
skipped DLC2dunFahlbtharzJournal02

This shouldn't be returning 0 from pos('DLC1ElderScroll','DLC2dunFahlbtharzJournal02')...but it is.

(btw - If you have Skyrim SE, you just need SSEEdit to test this.)

Here's my complete script:

{
  Dump book text
  Dumps "book text" property of every vanilla book

  Source for most copypasta:
  https://github.com/AngryAndConflict/skyrim-utils/
}
unit UserScript;
    uses mteFunctions;
    // uses RegExpr;
    
    function Initialize: integer;
    var
        f, group, e: IInterface;
        i, j: integer;
        btext,fname: String;
        slItems: TStringList;
        FileInfo: File_Content;
    begin
        AddMessage('Dumping books.....');

        slItems := TStringList.Create;

        // load item and perk stringlists
        for i := 0 to FileCount - 1 do begin
            f := FileByIndex(i);

            group := GroupBySignature(f, 'BOOK');
            for j := 0 to ElementCount(group) - 1 do begin
                e := ElementByIndex(group, j);
                slItems.Add(geev(e, 'EDID'));

                // if it's a book, do things
                if (isBook(e) = True) then begin
                    // this it the raw text content of each BOOK's DESC attribute
                    btext := geev(e,'DESC');
                    
                    if (Length(btext) > 0) then begin
                    
                        fname := ProgramPath + 'Output\' + (geev(e, 'EDID')) + '.txt';

                        // AddMessage('Successfully saved!');
                        // AddMessage(#9 + fname);
                        // AddMessage(#20);

                        // AddMessage('===ERROR=== Unable to save ' + fname + '!');
                    end;
                end;
            end;
        end;

        //AddMessage(btext)
    end;
    

    // check if item is book
    // exclude blacklisted books and spell tomes
    function isBook(item: IInterface): boolean;
    var
        formID,test: String;
        blacklist: TStringList;
        ignore: Boolean;
        i,t: integer;
    begin
        Result := false;
        ignore := false;

        blacklist := TStringList.Create;

        // blacklist to exclude - these are can be FormIDs or friendly names
        blacklist.Add('DLC1ElderScroll');
        blacklist.Add('DLC1FVBook01Falmer');
        blacklist.Add('DLC1FVBook02Falmer');
        blacklist.Add('DLC1FVBook03Falmer');
        blacklist.Add('DLC1FVBook04Falmer');
        blacklist.Add('DLC2BlackBook');
        blacklist.Add('DA04ElderScroll');
        blacklist.Add('ExpSpiderCrftBook');
        blacklist.Add('QA');
        blacklist.Add('Recipe');

        formID := geev(item,'Record Header\FormID');

        t := pos(blacklist(1),'Elder');
        test := IntToStr(t);
        AddMessage(test);

        for i := 0 to (blacklist.Count - 1) do begin
            if (pos(formID, blacklist(i)) >= 0) then begin
                AddMessage('====' + formID + '====');
                AddMessage(blacklist(i));
                // AddMessage('skipped ' + formID);

                ignore := true;
                Result := false;
                break;
            end;
        end;

        // has Keyword excludes all spell tomes
        if (Signature(item) = 'BOOK') and not (hasKeyword(item,'000937A5')) and not ignore then begin
            Result := true;
        end;
    end;
end.{
  Dump book text
  Dumps "book text" property of every vanilla book

  Source for most copypasta:
  https://github.com/AngryAndConflict/skyrim-utils/
}
unit UserScript;
    uses mteFunctions;
    // uses RegExpr;
    
    function Initialize: integer;
    var
        f, group, e: IInterface;
        i, j: integer;
        btext,fname: TString;
        slItems: TStringList;
        FileInfo: File_Content;
        blacklist: TStringList;
    begin
        AddMessage('Dumping books.....');

        slItems := TStringList.Create;

        // load item and perk stringlists
        for i := 0 to FileCount - 1 do begin
            f := FileByIndex(i);

            group := GroupBySignature(f, 'BOOK');
            for j := 0 to ElementCount(group) - 1 do begin
                e := ElementByIndex(group, j);
                slItems.Add(geev(e, 'EDID'));

                // if it's a book, do things
                if (isBook(e) = True) then begin
                    // this it the raw text content of each BOOK's DESC attribute
                    btext := geev(e,'DESC');
                    
                    if (Length(btext) > 0) then begin
                    
                        fname := ProgramPath + 'Output\' + (geev(e, 'EDID')) + '.txt';

                        // AddMessage('Successfully saved!');
                        // AddMessage(#9 + fname);
                        // AddMessage(#20);

                        // AddMessage('===ERROR=== Unable to save ' + fname + '!');
                    end;
                end;

                // AddMessage('----------')
            end;
        end;
        //AddMessage(btext)
    end;
    

    // check if item is book
    // exclude blacklisted books and spell tomes
    function isBook(item: IInterface): boolean;
    var
        formID,needle,haystack,test: TString;
        blacklist: TStringList;
        ignore: Boolean;
        i,t,p: integer;
    begin
        Result := false;
        ignore := false;

        blacklist := TStringList.Create;

        // blacklist to exclude - these are can be FormIDs or friendly names
        blacklist.Add('DLC1ElderScroll');
        blacklist.Add('DLC1FVBook01Falmer');
        blacklist.Add('DLC1FVBook02Falmer');
        blacklist.Add('DLC1FVBook03Falmer');
        blacklist.Add('DLC1FVBook04Falmer');
        blacklist.Add('DLC2BlackBook');
        blacklist.Add('DA04ElderScroll');
        blacklist.Add('ExpSpiderCrftBook');
        blacklist.Add('QA');
        blacklist.Add('Recipe');

        formID := geev(item,'EDID');

        for i := 0 to (blacklist.Count - 1) do begin
            needle := blacklist[i];
            haystack := formID;
            
            // AddMessage('====' + formID + '====');

            // AddMessage('looking in ' + haystack);
            // AddMessage(#9 + ' for ' + needle + '...');
            
            t := pos(haystack, needle);
            test := IntToStr(t);

            // AddMessage(test);

            if (pos(blacklist[i], formID) >= 0) then begin
                // AddMessage(blacklist[i]);
                AddMessage('skipped ' + formID);

                ignore := true;
                break;
            end;
        end;

        // hasKeyword excludes all spell tomes
        if (not (Signature(item) = 'BOOK')) or (hasKeyword(item,'000937A5')) then begin
            ignore := true;
        end;
        
        if ignore then begin
            Result := false;
        end;

        if not ignore then begin
            Result := true;
        end;
    end;
end.

Solution

  • Below is a rewrite of your script, set to only print messages without writing any file: if you're happy with how it works, just change line 14 to

      DRY_RUN = False;
    

    to have it actually dump the books content into the Output subfolder.

    I started from the latter of the two scripts you concatenated, I assumed that to be your latest version.

    Major changes:

    • populate blacklist only once inside Initialize, no need to do it for every record;
    • you don't need to check if your record is a book, because you're already iterating over the BOOK group, so I reversed the logic and only check if the editor ID contains any string in blacklist;
    • you don't really need mteFunctions, so I removed it and used the existing logic to blacklist records with SpellTome in their editor ID - if you ever need to check for keywords again, you can enable mteFunctions back and use its utilities;
    • check if a record IsMaster and then only work once on its WinningOverride - this avoids processing the same record multiple times in case is overridden by official DLCs or mods, reference: https://tes5edit.github.io/docs/13-Scripting-Functions.html#IwbMainRecord

    Minor changes:

    • call Free after using any TStringList;
    • add uses clauses for system units - not actually needed to run xEdit scripts, but useful to catch errors if you use a linter for your Delphi Pascal;
    • use Pred instead of Count - 1 - the result should be the same, but looks like the standard way to do iterations in Delphi world;
    • break lines before any begin and other similar changes following the style guide at: https://docwiki.embarcadero.com/RADStudio/Sydney/en/Delphi_Statements
    {
      Dump book text
      Dumps "book text" property of every book in loaded plugins
    
      Source for most copypasta:
      https://github.com/AngryAndConflict/skyrim-utils/
    }
    unit UserScript;
    
    interface
    
    const
      // change this to False to actually write the text files
      DRY_RUN = True;
      // change this to False if you want to continue after the first error
      STOP_ON_ERROR = True;
    
    implementation
    
    uses
      //mteFunctions,
      System.Classes,   // TStringList
      System.SysUtils,  // CreateDir, IntToStr
      xEditAPI;
    
    // check if rec editor ID contains any string in blacklist
    function IsBlacklisted(rec: IwbMainRecord; blacklist: TStringList): Boolean;
    var
      i: Cardinal;
      edid: string;
      //needle, haystack: string;
    begin
      Result := False;
      edid := EditorID(rec);
    
      for i := 0 to Pred(blacklist.Count) do
      begin
        //needle := blacklist[i];
        //haystack := edid;
        //AddMessage('====' + edid + '====');
        //AddMessage('looking in ' + haystack);
        //AddMessage(#9 + ' for ' + needle + '...');
        //AddMessage(' test: ' + IntToStr(Pos(needle, haystack));
    
        if (Pos(blacklist[i], edid) > 0) then
        begin
          Result := True;
          //AddMessage(blacklist[i]);
          AddMessage('Skipping: ' + edid);
          Break;
        end;
      end;
    end;
    
    function Initialize: Integer;
    var
      i, j: Cardinal;
      f: IwbFile;
      bookGroup: IwbGroupRecord;
      book: IwbMainRecord;
      btext, outputPath, fname: string;
      blacklist, output: TStringList;
    begin
      Result := 0;
      outputPath := ProgramPath + 'Output\';
    
      if not DRY_RUN then
      begin
        CreateDir(outputPath);
      end;
    
      blacklist := TStringList.Create;
    
      try
        // match editor IDs containing any of following
        blacklist.Add('DLC1ElderScroll');
        blacklist.Add('DLC1FVBook01Falmer');
        blacklist.Add('DLC1FVBook02Falmer');
        blacklist.Add('DLC1FVBook03Falmer');
        blacklist.Add('DLC1FVBook04Falmer');
        blacklist.Add('DLC2BlackBook');
        blacklist.Add('DA04ElderScroll');
        blacklist.Add('ExpSpiderCrftBook');
        blacklist.Add('QA');
        blacklist.Add('Recipe');
        blacklist.Add('SpellTome');
    
        AddMessage('Dumping books...');
    
        for i := 0 to Pred(FileCount) do
        begin
          f := FileByIndex(i);
          bookGroup := GroupBySignature(f, 'BOOK');
    
          for j := 0 to Pred(ElementCount(bookGroup)) do
          begin
            book := ElementByIndex(bookGroup, j);
    
            if IsMaster(book) then
            begin
              book := WinningOverride(book);
    
              // if it's not blacklisted, do things
              if (not IsBlacklisted(book, blacklist)) then
              begin
                // this it the raw text content of each BOOK's DESC attribute
                btext := GetElementEditValues(book, 'DESC');
    
                if (Length(btext) > 0) then
                begin
                  fname := outputPath + GetElementEditValues(book, 'EDID') + '.txt';
                  AddMessage('Outputting to: ' + fname);
    
                  if not DRY_RUN then
                  begin
                    output := TStringList.Create;
    
                    try
                      try
                        output.Add(btext);
                        output.SaveToFile(fname);
                        //AddMessage('Successfully saved!');
                        //AddMessage(#9 + fname);
                        //AddMessage(#20);
                      except
                        AddMessage('===ERROR=== Unable to save ' + fname + '!');
    
                        if STOP_ON_ERROR then
                        begin
                          raise;
                        end;
                      end;
                    finally
                      output.Free;
                    end;
                  end;
                end;
              end;
            end;
    
            //AddMessage('----------')
          end;
        end;
    
        //AddMessage(btext)
      finally
        blacklist.Free;
      end;
    end;
    
    end.