├── .gitattributes ├── .gitignore ├── Documentation ├── assets │ ├── banner.png │ └── main.css └── mxpf.html ├── ESPs ├── TestMXPF-1.esp ├── TestMXPF-2.esp └── TestMXPF-3.esp ├── Edit Scripts ├── MXPF - Rebalance Armors.pas ├── MXPF - Reweight Keys.pas ├── MXPF - Save Female NPCs - Quick.pas ├── MXPF - Save female NPCs.pas ├── MXPF - Tests.pas ├── jvTest - Example.pas └── lib │ ├── jvTest.pas │ ├── mteFunctions.pas │ └── mxpf.pas ├── Images ├── BuiltWithMXPF.png ├── BuiltWithMXPF.xcf ├── GettingStarted.png ├── GettingStarted.xcf ├── Resources.png ├── Resources.xcf ├── Screenshots │ ├── ss+(2016-01-16+at+02.33.29).png │ ├── ss+(2016-01-16+at+02.34.36).png │ └── ss+(2016-01-16+at+02.36.00).png ├── Thanks.png ├── Thanks.xcf ├── WhatIsMXPF.png ├── WhatIsMXPF.xcf ├── mxpf.png └── mxpf.xcf ├── README.md ├── license.txt └── test plan.txt /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Windows image file caches 2 | Thumbs.db 3 | ehthumbs.db 4 | 5 | # Folder config file 6 | Desktop.ini 7 | 8 | # Recycle Bin used on file shares 9 | $RECYCLE.BIN/ 10 | 11 | # Windows Installer files 12 | *.cab 13 | *.msi 14 | *.msm 15 | *.msp 16 | 17 | # Windows shortcuts 18 | *.lnk 19 | 20 | # ========================= 21 | # Operating System Files 22 | # ========================= 23 | 24 | # OSX 25 | # ========================= 26 | 27 | .DS_Store 28 | .AppleDouble 29 | .LSOverride 30 | 31 | # Thumbnails 32 | ._* 33 | 34 | # Files that might appear on external disk 35 | .Spotlight-V100 36 | .Trashes 37 | 38 | # Directories potentially created on remote AFP share 39 | .AppleDB 40 | .AppleDesktop 41 | Network Trash Folder 42 | Temporary Items 43 | .apdisk 44 | -------------------------------------------------------------------------------- /Documentation/assets/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matortheeternal/mxpf/26e834dfe3b00594b7d986ecd48b5f513226907a/Documentation/assets/banner.png -------------------------------------------------------------------------------- /Documentation/assets/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-size: 14px; 3 | line-height: 22px; 4 | background: #313131; 5 | color: #FFF; 6 | font-family: Helvetica Neue, Helvetica, Arial; 7 | } 8 | .interface { 9 | font-family: "Lucida Grande", "Lucida Sans Unicode", Helvetica, Arial, sans-serif !important; 10 | } 11 | div#sidebar { 12 | background: #222; 13 | position: fixed; 14 | top: 0; left: 0; bottom: 0; 15 | width: 200px; 16 | overflow-y: auto; 17 | overflow-x: hidden; 18 | -webkit-overflow-scrolling: touch; 19 | padding: 15px 0 30px 30px; 20 | border-right: 1px solid #222; 21 | box-shadow: 0 0 30px #555; -webkit-box-shadow: 0 0 30px #555; -moz-box-shadow: 0 0 30px #555; 22 | } 23 | a.toc_title, a.toc_title:visited { 24 | display: block; 25 | color: #7cc952; 26 | font-weight: bold; 27 | margin-top: 15px; 28 | } 29 | a.toc_title:hover { 30 | color: #fff; 31 | text-decoration: underline; 32 | } 33 | #sidebar .version { 34 | font-size: 10px; 35 | font-weight: normal; 36 | } 37 | ul.toc_section { 38 | font-size: 12px; 39 | line-height: 16px; 40 | margin: 5px 0 0 0; 41 | padding-left: 0px; 42 | list-style-type: none; 43 | } 44 | .toc_section li { 45 | margin: 0 0 6px 0; 46 | } 47 | .toc_section li a { 48 | text-decoration: none; 49 | color: white; 50 | } 51 | .toc_section li a:hover { 52 | text-decoration: underline; 53 | } 54 | div.container { 55 | width: 550px; 56 | margin: 40px 0 50px 260px; 57 | } 58 | img#logo { 59 | width: 396px; 60 | height: 69px; 61 | } 62 | div.warning { 63 | margin-top: 15px; 64 | font: bold 11px Arial; 65 | color: #770000; 66 | } 67 | br { 68 | margin-bottom: 6px; 69 | } 70 | p { 71 | margin: 20px 0; 72 | width: 550px; 73 | } 74 | a, a:visited { 75 | color: #62b7ff; 76 | } 77 | a:active, a:hover { 78 | color: #fff; 79 | } 80 | h1, h2, h3, h4, h5, h6 { 81 | padding-top: 20px; 82 | } 83 | h2 { 84 | color: #7cc952; 85 | font-size: 20px; 86 | } 87 | b.header { 88 | font-size: 16px; 89 | line-height: 30px; 90 | } 91 | span.alias { 92 | font-size: 14px; 93 | font-style: italic; 94 | margin-left: 20px; 95 | } 96 | table, tr, td { 97 | margin: 0; padding: 0; 98 | } 99 | td { 100 | padding: 2px 12px 2px 0; 101 | } 102 | table .rule { 103 | height: 1px; 104 | background: #ccc; 105 | margin: 5px 0; 106 | } 107 | ul { 108 | list-style-type: circle; 109 | padding: 0 0 0 20px; 110 | } 111 | li { 112 | margin-bottom: 10px; 113 | } 114 | code, pre, tt { 115 | font-family: Monaco, Consolas, "Lucida Console", monospace; 116 | font-size: 12px; 117 | line-height: 18px; 118 | font-style: normal; 119 | } 120 | tt { 121 | padding: 0px 3px; 122 | background: #444; 123 | border: 1px solid #111; 124 | zoom: 1; 125 | } 126 | code { 127 | margin-left: 20px; 128 | } 129 | pre { 130 | font-size: 12px; 131 | padding: 2px 0 2px 15px; 132 | border-left: 5px solid #bbb; 133 | margin: 0px 0 30px; 134 | } 135 | @media only screen and (-webkit-min-device-pixel-ratio: 1.5) and (max-width: 640px), 136 | only screen and (-o-min-device-pixel-ratio: 3/2) and (max-width: 640px), 137 | only screen and (min-device-pixel-ratio: 1.5) and (max-width: 640px) { 138 | img { 139 | max-width: 100%; 140 | } 141 | div#sidebar { 142 | -webkit-overflow-scrolling: initial; 143 | position: relative; 144 | width: 90%; 145 | height: 120px; 146 | left: 0; 147 | top: -7px; 148 | padding: 10px 0 10px 30px; 149 | border: 0; 150 | } 151 | img#logo { 152 | width: auto; 153 | height: auto; 154 | } 155 | div.container { 156 | margin: 0; 157 | width: 100%; 158 | } 159 | p, div.container ul { 160 | max-width: 98%; 161 | overflow-x: scroll; 162 | } 163 | pre { 164 | overflow: scroll; 165 | } 166 | } -------------------------------------------------------------------------------- /Documentation/mxpf.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Mator's xEdit Patching Framework 11 | 12 | 13 | 14 | 15 | 145 | 146 |
147 |

148 | 149 |

MXPF is an xEdit scripting library that provides a variety of functions 150 | for the generation of patches. Want to rebalance armor and weapons in 151 | Skyrim? Done. Apply a new particle effect to every weather? Easy. 152 | Change the configuration of all NPCs? Cake. 153 |

154 | 155 |

MXPF is comprised a suite of functions that do the work of finding the 156 | records you want to patch, identifying/creating a patch file, and 157 | copying records to the patch. Where you would normally have to learn 158 | to do all of these steps yourself, with MXPF you can accomplish 159 | everything you need with a few simple functions.

160 | 161 |

The project is available on GitHub and Nexus Mods. A test suite is 162 | available. There are also a bunch of example scripts available which show 163 | you how you can use MXPF and xEdit Scripting to solve problems.

164 | 165 |

You can report bugs and discuss features on the GitHub issues page. 166 | You can also talk with developers and other users on the xEdit HipChat, 167 | in the NexusMods comments section, and on r/xEdit.

168 | 169 |

Dependencies

170 | 189 | 190 |

Download and Setup

191 | You can download MXPF from 192 | GitHub or 193 | Nexus Mods. 194 | Once downloaded, you will need to copy the contents of the included 195 | Edit Scripts folder into xEdit's Edit Scripts folder. Once you've 196 | done that, you're set! You can then use MXPF in any script by 197 | putting uses mxpf; after the unit declaration. 198 | 199 |

Running Tests

200 | You can run the MXPF tests through TES5Edit. Note: You must have 201 | Skyrim installed to run the MXPF tests. 202 |
    203 |
  1. Install the TestMXPF ESP files into your data directory. If you're 204 | using a mod manager you could also, alternatively, install them as a new 205 | mod.
  2. 206 |
  3. Load Skyrim.esm, TestMXPF-1.esp, TestMXPF-2.esp, and TestMXPF-3.esp 207 | into TES5Edit.
  4. 208 |
  5. Wait for the background loader to complete.
  6. 209 |
  7. Right-click any file in TES5Edit and choose Apply Script.
  8. 210 |
  9. Select MXPF - Tests from the dropdown menu and click OK.
  10. 211 |
  11. Wait for tests to complete. The log will be printed to TES5Edit's 212 | messages tab and will also be saved to Edit Scripts\jvTest\
  12. 213 |
214 | 215 |

User Constants

216 | User constants are constants that are meant to be set by the user. 217 | They will vary from machine to machine. 218 | 219 |

220 | mxDebug boolean 221 |
222 | If set to true MXPF will generate debug messages throughout program 223 | execution. Use mxSaveDebug, mxPrintDebug or 224 | mxEchoDebug to control where debug information is sent. 225 |

226 | 227 |

228 | mxDebugVerbose boolean 229 |
230 | If set to true MXPF will generate verbose debug messages. These are 231 | messages associated with every record processed by MXPF. Tracking 232 | these messages will slow down MXPF execution and may make debug logs 233 | extremely large. 234 |

235 | 236 |

237 | mxSaveDebug boolean 238 |
239 | If set to true and mxDebug is enabled, debug messages will be 240 | saved to a text document in mxpf/logs/mxpf-debug-{{time}}.txt. 241 |

242 | 243 |

244 | mxSaveFailures boolean 245 |
246 | If set to true, failure messages will be saved to a text document in 247 | mxpf/logs/mxpf-failures-{{time}}.txt. 248 |

249 | 250 |

251 | mxPrintDebug boolean 252 |
253 | If set to true and mxDebug is enabled, debug messages will be 254 | printed to xEdit's message log when FinalizeMXPF is called. 255 |

256 | 257 |

258 | mxPrintFailures boolean 259 |
260 | If set to true, failure messages will be printed to xEdit's message log 261 | when FinalizeMXPF is called. 262 |

263 | 264 |

265 | mxEchoDebug boolean 266 |
267 | If set to true and mxDebug is enabled, debug messages will be 268 | printed to xEdit's message log during execution. Echoing messages slows 269 | down script execution. 270 |

271 | 272 |

273 | mxEchoFailures boolean 274 |
275 | If set to true, failure messages will be printed to xEdit's message log 276 | during execution. Echoing messages slows down script execution. 277 |

278 | 279 | 280 |

Developer Constants

281 | Developer constants are constants that shouldn't be changed. These constants 282 | will be the same on every machine per version of MXPF, and can be useful 283 | in scripts that use MXPF. 284 | 285 |

286 | mxVersion string 287 |
288 | This is a string representing the version of the MXPF file being used. 289 | You can check this to make sure that MXPF is of an adequate version for 290 | your script. 291 |

292 | 293 |

294 | mxBethesdaSkyrimFiles string 295 |
296 | This is a string containing a comma-separated list of the filenames of 297 | all Bethesda master files associated with Skyrim. 298 |

299 | 300 |

301 | mxHardcodedDatFiles string 302 |
303 | This is a string containing a comma-separated list of the filenames of 304 | all hardcoded dat files that xEdit uses to emulate game executables. 305 | These files appears as Skyrim.exe, Fallout3.exe, Oblivion.exe, and 306 | FalloutNV.exe in xEdit. 307 |

308 | 309 | 310 |

Variables

311 | 312 |

313 | mxLoadMasterRecords boolean 314 |
315 | Setting this to true will alter how LoadRecords behaves. When true, 316 | LoadRecords will only load records that are masters (i.e. new records 317 | in the file they're being processed in, not overrides). This allows you 318 | to avoid patching records that override records from files you've excluded 319 | or not included. 320 |

321 | 322 |

323 | mxLoadOverrideRecords boolean 324 |
325 | Setting this to true will alter how LoadRecords behaves. When true, 326 | LoadRecords will only load records that are override records (i.e. 327 | not new records). This is the opposite of mxLoadMasterRecords, and 328 | is useful if you want to only target override records in a few specific 329 | files. Note that using this option and mxLoadMasterRecords at 330 | the same time will result in no records being loaded. 331 |

332 | 333 |

334 | mxLoadWinningOverrides boolean 335 |
336 | Setting this to true will alter how LoadRecords behaves. 337 | When true, LoadRecords will load the winning overrides of records, 338 | which makes certain you don't overwrite the changes made to the record 339 | that would be present in game without the patch file you're generating. 340 | This is extremely important, and should almost always be set to true. 341 | This option can be used in conjunction with mxLoadMasterRecords 342 | or mxLoadOverrideRecords. 343 |

344 | 345 |

346 | mxSkipPatchedRecords boolean 347 |
348 | Setting this to true will have CopyRecordToPatch and 349 | CopyRecordsToPatch skip copying records that are already 350 | present in mxPatchFile. This is useful if you want to avoid 351 | performing duplicate work or patching the same value twice. 352 |

353 | 354 |

355 | mxPatchFile IInterface 356 |
357 | Holds the patch file used by MXPF. This is useful to access if you 358 | need to change some attributes of the patch file created by your 359 | script. 360 |

361 | 362 | 363 |

General

364 | 365 |

366 | InitializeMXPF procedure InitializeMXPF; 367 |
368 | Initializes MXPF. You must call this at the beginning of every script 369 | before attempting to access any other MXPF function (except 370 | DefaultOptionsMXPF). 371 |

372 | 373 |

374 | DefaultOptionsMXPF procedure DefaultOptionsMXPF; 375 |
376 | Sets default options in MXPF. Usually you should call this whenever you 377 | use MXPF, as it sets the options that most use cases require. This method 378 | sets mxLoadMasterRecords, mxLoadWinningOverrides and 379 | mxSkipPatchedRecords to true. 380 |

381 | 382 |

383 | FinalizeMXPF procedure FinalizeMXPF; 384 |
385 | Finalizes MXPF. You must call this at the end of every script you've 386 | used MXPF in. Calling this function frees all memory used by MXPF, 387 | cleans the masters of mxPatchFile if it is assigned, and 388 | prints/saves debug and failure messages, depending on user constants. 389 |

390 |
unit userscript;
391 | 
392 | uses mxpf;
393 | 
394 | function Initialize: integer;
395 | begin
396 |   InitializeMXPF;
397 |   DefaultOptionsMXPF;
398 |   { Everything you want to do with MXPF goes here. }
399 |   FinalizeMXPF;
400 | end;
401 | 402 |

Patch File Selection

403 | 404 |

405 | PatchFileByAuthor procedure PatchFileByAuthor(author: string); 406 |
407 | Selects the MXPF patch file by an author. If no file is 408 | found in the user's load order matching the specified author, the method 409 | will prompt the user to create one, and set the new file's author to the 410 | specified author string.
411 | 412 | Once selected, the patch file is stored in the mxPatchFile variable, 413 | and is used by MXPF Record Patching methods. Using author based file 414 | selection is preferable over filename selection because xEdit does not 415 | currently support a means of creating a file with a specified name (the 416 | user has to enter the name manually). Author selection also allows the 417 | user to have multiple instances of the patch file in their load order 418 | without having to manually rename them to avoid conflicts.
419 | 420 | NOTE: If there are multiple files with authors matching the specified 421 | author, this method will select the one closest to the top of the user's 422 | load order. 423 |

424 |
PatchFileByAuthor('MXPF ARMO Example');
425 | 
426 | 427 |

428 | PatchFileByName procedure PatchFileByName(filename: string); 429 |
430 | Selects the MXPF patch file by a filename. If no file is found 431 | in the user's load order matching the specified filename, the method will 432 | prompt the user to create one. If the created file does not match the 433 | desired filename, the script will inform the user that it will not 434 | recognize that file as the patch file in the future.
435 | Once selected, the patch file is stored in the mxPatchFile variable, 436 | and is used by MXPF Record Patching methods. Using filename based file 437 | selection is fairly standard, but selecting by author is better because 438 | it is more flexible. 439 |

440 |
PatchFileByName('MXPF ARMO Example.esp');
441 | 
442 | 443 |

File Selection

444 | 445 |

446 | SetExclusions procedure SetExclusions(s: string); 447 |
448 | Sets the file selection mode to exclusion and loads the comma-separated 449 | list of files, s, into the mxFiles stringlist. Affects which 450 | files LoadRecords will load records from. 451 |

452 |
PatchFileByAuthor('MXPF ARMO Example');
453 | SetExclusions(mxHardcodedDatFiles);
454 | // now we won't load records from any of the hardcoded dat files
455 | LoadRecords('ARMO');
456 | 
457 | 458 |

459 | SetInclusions procedure SetInclusions(s: string); 460 |
461 | Sets the file selection mode to inclusion and loads the comma-separated 462 | list of files, s, into the mxFiles stringlist. Affects which 463 | files LoadRecords will load records from. 464 |

465 |
PatchFileByAuthor('MXPF ARMO Example');
466 | SetInclusions('Skyrim.esm');
467 | // now we will only load records from Skyrim.esm
468 | LoadRecords('ARMO');
469 | 
470 | 471 |

Record Processing

472 | 473 |

474 | LoadRecords procedure LoadRecords(sig: string); 475 |
476 | Loads records from the file selection specified with SetInclusions or 477 | SetExclusions. All records matching the input signature, sig 478 | will be added to the mxRecords list. Note: Calling this function 479 | does not clear the mxRecords list. 480 |

481 |
SetExclusions(mxHardcodedDatFiles);
482 | LoadRecords('WOOP'); // loads all WOOP (word of power) records
483 | LoadRecords('OTFT'); // loads all OTFT (outfit) records
484 | 
485 | 486 |

487 | LoadChildRecords procedure LoadChildRecords(groupSig, sig: string); 488 |
489 | Loads records from the file selection specified with SetInclusions 490 | or SetExclusions. The group matching the input signature, 491 | groupSig will be searched for records matching the input signature 492 | sig. Found records will be added to the mxRecords list. You 493 | must use this function instead of LoadRecords when loading records 494 | from child groups. These are Dialogue Topic Info records (INFO) and any 495 | records that are children of Cell or Worldspace records (NAVM, ACHR, REFR, etc.). 496 |

497 |
SetExclusions(mxHardcodedDatFiles);
498 | // load all NAVM (navmesh) records from the WRLD (worldspace) group
499 | LoadChildRecords('WRLD', 'NAVM');
500 | // load all REFR (object reference) records from the CELL (cell) group
501 | LoadChildRecords('CELL', 'REFR');
502 | 
503 | 504 |

505 | GetRecord function GetRecord(i: integer): IInterface; 506 |
507 | Accesses records that were loaded through the LoadRecords function. 508 | Takes an index, i, to determine which record to load. You should 509 | use this function in conjunction with MaxRecordIndex. 510 |

511 |
LoadRecords('STAT');
512 | for i := 0 to MaxRecordIndex do begin
513 |   rec := GetRecord(i);
514 |   AddMessage('Found '+Name(rec));
515 | end;
516 | 
517 | 518 |

519 | RemoveRecord procedure RemoveRecord(i: integer); 520 |
521 | Removes the record at the specified index from the mxRecords 522 | list. Use this to apply additional filtering to records you want to 523 | patch after calling LoadRecords and before calling 524 | CopyRecordsToPatch. NOTE: You should always loop from the 525 | maximum index downto 0 when removing elements from the set 526 | you're looping through. 527 |

528 |
LoadRecords('WEAP');
529 | for i := MaxRecordIndex downto 0 do begin
530 |   rec := GetRecord(i);
531 |   damage := StrToInt(GetElementEditValues(rec, 'DATA\Damage'));
532 |   // remove the weapon if its damage is less than 20
533 |   if damage < 20 then
534 |     RemoveRecord(i);
535 | end;
536 | 
537 | 538 |

539 | MaxRecordIndex function MaxRecordIndex: integer; 540 |
541 | Gets the number of items in mxRecords minus 1, the maximum 542 | index you can call GetRecord with successfully. This is for 543 | use when looping through loaded records. 544 |

545 | 546 |

Record Patching

547 | 548 |

549 | CopyRecordToPatch function CopyRecordToPatch(i: integer): IInterface; 550 |
551 | Copies the record at index i in mxRecords to the 552 | mxPatchFile. Adds required masters to mxPatchFile 553 | if they haven't been added yet. This provides more control than 554 | CopyRecordsToPatch. 555 |

556 |
LoadRecords('NPC_');
557 | for i := 0 to MaxRecordIndex do begin
558 |   rec := GetRecord(i);
559 |   // instead of using RemoveRecord, we can just copy specific
560 |   // records with CopyRecordToPatch
561 |   if GetElementEditValues(rec, 'ACBS\Flags\Female') = '1' then
562 |     CopyRecordToPatch(i);
563 | end;
564 | 
565 | 566 |

567 | CopyRecordsToPatch procedure CopyRecordsToPatch; 568 |
569 | Copies all loaded records in mxRecords to the 570 | mxPatchFile. Adds required masters to mxPatchFile 571 | if they haven't been added yet. Records that fail to copy will be 572 | logged to mxFailures. Records that are copied successfully 573 | will be added to mxPatchRecords, and will be accessible 574 | through GetPatchRecord. 575 |

576 | 577 |

578 | GetPatchRecord function GetPatchRecord(i: integer): IInterface; 579 |
580 | Accesses records that were copied through the 581 | CopyRecordToPatch and CopyRecordsToPatch functions. 582 | Takes an index, i, to determine which record to load. You should 583 | use this function in conjunction with MaxPatchRecordIndex. 584 |

585 |
LoadRecords('LSCR');
586 | CopyRecordsToPatch;
587 | for i := 0 to MaxPatchRecordIndex do begin
588 |   rec := GetPatchRecord(i);
589 |   AddMessage('Patched '+Name(rec));
590 | end;
591 | 
592 | 593 |

594 | MaxPatchRecordIndex function MaxPatchRecordIndex: integer; 595 |
596 | Gets the number of items in mxPatchRecords minus 1, the 597 | maximum index you can call GetPatchRecord with successfully. 598 | This is for use when looping through patch records. 599 |

600 | 601 |

Reporting

602 | 603 |

604 | PrintMXPFReport procedure PrintMXPFReport; 605 |
606 | Prints a message to log the reporting on the number of copy operations 607 | performed, the number that were successful, and the number that failed. 608 |

609 |
LoadRecords('INGR');
610 | CopyRecordsToPatch;
611 | PrintMXPFReport;
612 | 
613 | 614 |

Macros

615 | 616 |

617 | MultiFileSelectString function MultiFileSelectString(sPrompt: String; var sFiles: String): Boolean; 618 |
619 | Uses MultiFileSelect from mteFunctions and outputs the result as a comma-separated string through sFiles. Returns false if the user cancelled the file selection. 620 |

621 |
bCanceled := not MultiFileSelectString('Select some files.', sFiles);
622 | if bCanceled then exit;
623 | SetInclusions(sFiles);
624 | 
625 | 626 |

627 | SetFileSelection procedure SetFileSelection(sFiles: String; bMode: Boolean); 628 |
629 | If bMode is true, calls SetInclusions on sFiles. If bMode is false, calls SetExclusions on sFiles. 630 |

631 |
sFiles := 'Skyrim.esm,Dawnguards.esm,Dragonborn.esm';
632 | SetFileSelection(sFiles, false); // excluding the files
633 | SetFileSelection(sFiles, true); // including the files
634 | 
635 | 636 |

637 | MultiLoad procedure MultiLoad(sRecords: String); 638 |
639 | Accepts a comma-separated string of record signatures sRecords. Can also accept signature pairs. Calls LoadRecords for each record signature in sRecords, and calls LoadChildRecords for each record signature pair. 640 |

641 |
// load ARMO, ARMA, and WEAP records from the file selection
642 | MultiLoad('ARMO,ARMA,WEAP');
643 | // load REFR and ACHR children records from CELL record groups in the
644 | // file selection
645 | MultiLoad('CELL:REFR,CELL:ACHR');
646 | 
647 | 648 |

649 | QuickLoad procedure QuickLoad(sFiles, sRecords: String; bMode: Boolean); 650 |
651 | Calls InitializeMXPF and DefaultOptionsMXPF. Then calls SetFileSelection with the input sFiles and bMode, and calls MultiLoad with the input sRecords. 652 |

653 |
// load all ACHR records from the CELL and WRLD record groups in all loaded 
654 | // non-bethesda files.
655 | QuickLoad(mxBethesdaSkyrimFiles, 'CELL:ACHR,WRLD:ACHR', false);
656 | // loop through loaded records
657 | for i := 0 to MaxRecordIndex do begin
658 |   rec := GetRecord(i);
659 |   npc := LinksTo(ElementByPath(rec, 'NAME'));
660 |   AddMessage('Found placed NPC '+Name(npc));
661 | end;
662 | 
663 | 664 |

665 | QuickPatch procedure QuickPatch(sAuthor, sFiles, sRecords: String; bMode: Boolean); 666 |
667 | Calls InitializeMXPF and DefaultOptionsMXPF. Then calls PatchFileByAuthor with the input sAuthor. Then calls SetFileSelection with the input sFiles and bMode, and calls MultiLoad with the input sRecords. Finally, calls CopyRecordsToPatch. 668 |

669 |
// load all AMMO, ARMO and WEAP records from Skyrim.esm, and copy them to 
670 | // a patch file with the author "Rebalance"
671 | QuickPatch('Rebalance', 'Skyrim.esm', 'AMMO,ARMO,WEAP', true);
672 | // loop through records in patch
673 | for i := 0 to MaxPatchRecordIndex do begin
674 |   rec := GetPatchRecord(i);
675 |   sig := Signature(rec);
676 |   // double the armor rating of all armors
677 |   if sig = 'ARMO' then begin
678 |     oldValue := GetElementNativeValues(rec, 'DNAM');
679 |     SetElementNativeValues(rec, 'DNAM', oldValue * 2.0)
680 |   end
681 |   // double the damage of all weapons and ammo
682 |   else begin
683 |     oldValue := GetElementNativeValues(rec, 'DATA\Damage');
684 |     SetElementNativeValues(rec, 'DATA\Damage', oldValue * 2.0);
685 |   end;
686 | end;
687 | FinalizeMXPF;
688 | 
689 | 690 | 691 | 692 | 721 | 722 |

Changelog

723 | 724 |

725 | 0.1.0August 31, 2015 – 726 | Docs 727 |
Initial release of MXPF. 728 |

729 |
730 | 731 | 732 | -------------------------------------------------------------------------------- /ESPs/TestMXPF-1.esp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matortheeternal/mxpf/26e834dfe3b00594b7d986ecd48b5f513226907a/ESPs/TestMXPF-1.esp -------------------------------------------------------------------------------- /ESPs/TestMXPF-2.esp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matortheeternal/mxpf/26e834dfe3b00594b7d986ecd48b5f513226907a/ESPs/TestMXPF-2.esp -------------------------------------------------------------------------------- /ESPs/TestMXPF-3.esp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matortheeternal/mxpf/26e834dfe3b00594b7d986ecd48b5f513226907a/ESPs/TestMXPF-3.esp -------------------------------------------------------------------------------- /Edit Scripts/MXPF - Rebalance Armors.pas: -------------------------------------------------------------------------------- 1 | { 2 | MXPF - Rebalance Armors 3 | by matortheeternal 4 | 5 | Sample MXPF Script, sets all heavy armor to have 25% higher 6 | armor rating. 7 | } 8 | 9 | unit UserScript; 10 | 11 | uses 'lib\mxpf'; 12 | 13 | function Initialize: Integer; 14 | var 15 | i: integer; 16 | armorRating, newArmorRating: real; 17 | rec: IInterface; 18 | begin 19 | // set MXPF options and initialize it 20 | DefaultOptionsMXPF; 21 | InitializeMXPF; 22 | 23 | // select/create a new patch file that will be identified by its author field 24 | PatchFileByAuthor('TestMXPF'); 25 | SetExclusions(mxBethesdaSkyrimFiles); // excludes bethesda files from record loading 26 | LoadRecords('ARMO'); // loads all Armor records 27 | 28 | // you can filter the loaded records like this 29 | // it's important that the loop starts at MaxRecordIndex and goes down to 0 30 | // because we're removing records 31 | for i := MaxRecordIndex downto 0 do begin 32 | rec := GetRecord(i); 33 | // remove records that don't have the ArmorLight keyword 34 | if not HasKeyword(rec, 'ArmorLight') then 35 | RemoveRecord(i) 36 | // remove records with DNAM - Armor Rating = 0 37 | else if (genv(rec, 'DNAM - Armor Rating') = 0) then 38 | RemoveRecord(i); 39 | end; 40 | 41 | // then copy records to the patch file 42 | CopyRecordsToPatch; 43 | 44 | // and set values on them 45 | for i := 0 to MaxPatchRecordIndex do begin 46 | rec := GetPatchRecord(i); 47 | armorRating := StrToFloat(geev(rec, 'DNAM')); 48 | newArmorRating := Int(armorRating * 1.25); 49 | AddMessage(Format('Changed armor rating from %0.2f to %0.2f on %s', [armorRating, newArmorRating, Name(rec)])); 50 | seev(rec, 'DNAM', FloatToStr(newArmorRating)); 51 | end; 52 | 53 | // call PrintMXPFReport for a report on successes and failures 54 | PrintMXPFReport; 55 | 56 | // always call FinalizeMXPF when done 57 | FinalizeMXPF; 58 | end; 59 | 60 | end. 61 | -------------------------------------------------------------------------------- /Edit Scripts/MXPF - Reweight Keys.pas: -------------------------------------------------------------------------------- 1 | { 2 | MXPF - Reweight Keys 3 | by matortheeternal 4 | 5 | Sample MXPF Script, sets Key record weights to 0.05. 6 | 7 | This script makes use of the QuickPatch macro, which allows 8 | it to work in 3 lines of code. 9 | } 10 | 11 | unit UserScript; 12 | 13 | uses 'lib\mxpf'; 14 | 15 | function Initialize: Integer; 16 | var 17 | i: integer; 18 | begin 19 | QuickPatch('MXPF - Reweight Keys', mxHardcodedDatFiles, 'KEYM', false); 20 | for i := MaxPatchRecordIndex downto 0 do 21 | seev(GetPatchRecord(i), 'DATA\Weight', '0.05'); 22 | FinalizeMXPF; 23 | end; 24 | 25 | end. 26 | -------------------------------------------------------------------------------- /Edit Scripts/MXPF - Save Female NPCs - Quick.pas: -------------------------------------------------------------------------------- 1 | { 2 | MXPF - Save Female NPCs - Quick 3 | by matortheeternal 4 | 5 | Sample script for MXPF which is the same as the Save 6 | Female NPCs script, but makes use of the MXPF macros 7 | MultiFileSelectString and QuickLoad. 8 | } 9 | 10 | unit UserScript; 11 | 12 | uses 'lib\mxpf'; 13 | 14 | function Initialize: Integer; 15 | var 16 | sl: TStringList; 17 | sFiles: String; 18 | i: integer; 19 | rec: IInterface; 20 | begin 21 | // get file selection from user 22 | if not MultiFileSelectString('Select the files you want to load Female NPCs from', sFiles) then 23 | exit; // if user cancels, exit 24 | 25 | // set up MXPF and load NPC_ records from files the user selected 26 | QuickLoad(sFiles, 'NPC_', true); 27 | 28 | // initialize stringlist which will hold a list of female NPCs we find 29 | sl := TStringList.Create; 30 | 31 | // add names of female NPCs to the stringlist 32 | for i := 0 to MaxRecordIndex do begin 33 | rec := GetRecord(i); 34 | if geev(rec, 'ACBS/Flags/Female') = '1' then 35 | sl.Add(Name(rec)); 36 | end; 37 | 38 | // clean up 39 | FinalizeMXPF; 40 | sl.SaveToFile('Female NPCs.txt'); 41 | sl.Free; 42 | end; 43 | 44 | end. -------------------------------------------------------------------------------- /Edit Scripts/MXPF - Save female NPCs.pas: -------------------------------------------------------------------------------- 1 | { 2 | MXPF - Save Female NPCs 3 | by matortheeternal 4 | 5 | Sample MXPF script which saves a list of female NPCs 6 | from a user-selected list of a files to a text document 7 | in the same directory as TES5Edit.exe. 8 | } 9 | 10 | unit UserScript; 11 | 12 | uses 'lib\mxpf'; 13 | 14 | function Initialize: Integer; 15 | var 16 | i: integer; 17 | sFiles: String; 18 | sl: TStringList; 19 | rec: IInterface; 20 | begin 21 | // get file selection from user 22 | if not MultiFileSelectString('Select the files you want to load Female NPCs from', sFiles) then 23 | exit; // if user cancels, exit 24 | 25 | // use MXPF to load NPC_ records from the user's file selection 26 | InitializeMXPF; 27 | DefaultOptionsMXPF; 28 | SetInclusions(sFiles); 29 | LoadRecords('NPC_'); 30 | 31 | // initialize stringlist which will hold a list of female NPCs we find 32 | sl := TStringList.Create; 33 | 34 | // add names of female NPCs to the stringlist 35 | for i := 0 to MaxRecordIndex do begin 36 | rec := GetRecord(i); 37 | if geev(rec, 'ACBS/Flags/Female') = '1' then 38 | sl.Add(Name(rec)); 39 | end; 40 | 41 | // clean up 42 | FinalizeMXPF; 43 | sl.SaveToFile('Female NPCs.txt'); 44 | sl.Free; 45 | end; 46 | 47 | end. -------------------------------------------------------------------------------- /Edit Scripts/MXPF - Tests.pas: -------------------------------------------------------------------------------- 1 | unit UserScript; 2 | 3 | uses 'lib\mxpf', 'lib\jvTest'; 4 | 5 | 6 | {******************************************************************************} 7 | { TEST GENERAL 8 | Tests general MXPF functions: 9 | - InitializeMXPF 10 | - DefaultOptionsMXPF 11 | - FinalizeMXPF 12 | } 13 | {******************************************************************************} 14 | 15 | procedure TestGeneral; 16 | begin 17 | Describe('General'); 18 | try 19 | Describe('InitializeMXPF'); 20 | try 21 | InitializeMXPF; 22 | Expect(mxInitialized, 'Should set mxInitialized to true'); 23 | Expect(Assigned(mxDebugMessages), 'Should create mxDebugMessages'); 24 | Expect(Assigned(mxFailureMessages), 'Should create mxFailureMessages'); 25 | Expect(Assigned(mxMasters), 'Should create mxMasters'); 26 | Expect(Assigned(mxFiles), 'Should create mxFiles'); 27 | Expect(Assigned(mxRecords), 'Should create mxRecords'); 28 | Expect(Assigned(mxPatchRecords), 'Should create mxPatchRecords'); 29 | Pass; 30 | except 31 | on x: Exception do Fail(x); 32 | end; 33 | 34 | Describe('DefaultOptionsMXPF'); 35 | try 36 | DefaultOptionsMXPF; 37 | Expect(mxLoadMasterRecords, 'Should set mxLoadMasterRecords to true'); 38 | Expect(mxSkipPatchedRecords, 'Should set mxSkipPatchedRecords to true'); 39 | Expect(mxLoadWinningOverrides, 'Should set mxLoadWinningOverrides to true'); 40 | Pass; 41 | except 42 | on x: Exception do Fail(x); 43 | end; 44 | 45 | Describe('FinalizeMXPF'); 46 | try 47 | FinalizeMXPF; 48 | Expect(not mxInitialized, 'Should set mxInitialized to false'); 49 | Expect(not mxLoadCalled, 'Should set mxLoadCalled to false'); 50 | Expect(not mxCopyCalled, 'Should set mxCopyCalled to false'); 51 | Expect(not mxLoadMasterRecords, 'Should set mxLoadMasterRecords to false'); 52 | Expect(not mxLoadOverrideRecords, 'Should set mxLoadOverrideRecords to false'); 53 | Expect(not mxLoadWinningOverrides, 'Should set mxLoadWinningOverrides to false'); 54 | ExpectEqual(mxFileMode, 0, 'Should set mxFileMode to 0'); 55 | ExpectEqual(mxRecordsCopied, 0, 'Should set mxRecordsCopied to 0'); 56 | Expect(not Assigned(mxPatchFile), 'Should set mxPatchFile to nil'); 57 | Expect(not Assigned(mxDebugMessages), 'Should free mxDebugMessages'); 58 | Expect(not Assigned(mxFailureMessages), 'Should free mxFailureMessages'); 59 | Expect(not Assigned(mxFiles), 'Should free mxFiles'); 60 | Expect(not Assigned(mxMasters), 'Should free mxMasters'); 61 | Expect(not Assigned(mxRecords), 'Should free mxRecords'); 62 | Expect(not Assigned(mxPatchRecords), 'Should free mxPatchRecords'); 63 | Pass; 64 | except 65 | on x: Exception do Fail(x); 66 | end; 67 | 68 | Pass; 69 | except 70 | on x: Exception do Fail(x); 71 | end; 72 | end; 73 | 74 | 75 | {******************************************************************************} 76 | { TEST LOGGING 77 | Tests logging MXPF functions: 78 | - DebugMessage 79 | - DebugList 80 | - FailureMessage 81 | } 82 | {******************************************************************************} 83 | 84 | procedure TestLogging; 85 | var 86 | sl: TStringList; 87 | begin 88 | Describe('Logging'); 89 | try 90 | Describe('DebugMessage'); 91 | try 92 | InitializeMXPF; 93 | ExpectEqual(mxDebugMessages.Count, 2, 'Should have 2 messages after InitializeMXPF has been called'); 94 | DebugMessage('Test Message'); 95 | ExpectEqual(mxDebugMessages[2], 'Test Message', 'Messages should be stored correctly'); 96 | FinalizeMXPF; 97 | Pass; 98 | except 99 | on x: Exception do begin 100 | if mxInitialized then FinalizeMXPF; 101 | Fail(x); 102 | end; 103 | end; 104 | 105 | // create stringlist for DebugList 106 | sl := TStringList.Create; 107 | sl.Add('Apple'); 108 | sl.Add('Orange'); 109 | 110 | Describe('DebugList'); 111 | try 112 | // Test with no prefix 113 | Describe('No prefix'); 114 | try 115 | InitializeMXPF; 116 | DebugList(sl, ''); 117 | ExpectEqual(mxDebugMessages[2], 'Apple', 'First message should match'); 118 | ExpectEqual(mxDebugMessages[3], 'Orange', 'Second message should match'); 119 | FinalizeMXPF; 120 | Pass; 121 | except 122 | on x: Exception do begin 123 | if mxInitialized then FinalizeMXPF; 124 | Fail(x); 125 | end; 126 | end; 127 | 128 | // Test with prefix 129 | Describe('With prefix'); 130 | try 131 | InitializeMXPF; 132 | DebugList(sl, 'TEST: '); 133 | ExpectEqual(mxDebugMessages[2], 'TEST: Apple', 'First message should match'); 134 | ExpectEqual(mxDebugMessages[3], 'TEST: Orange', 'Second message should match'); 135 | FinalizeMXPF; 136 | Pass; 137 | except 138 | on x: Exception do begin 139 | if mxInitialized then FinalizeMXPF; 140 | Fail(x); 141 | end; 142 | end; 143 | 144 | // All tests passed 145 | Pass; 146 | except 147 | on x: Exception do Fail(x); 148 | end; 149 | 150 | // free stringlist 151 | sl.Free; 152 | 153 | Describe('FailureMessage'); 154 | try 155 | InitializeMXPF; 156 | ExpectEqual(mxFailureMessages.Count, 0, 'Should have 0 messages after InitializeMXPF has been called'); 157 | FailureMessage('Example Failure'); 158 | ExpectEqual(mxFailureMessages[0], 'Example Failure', 'Messages should be stored correctly'); 159 | FinalizeMXPF; 160 | Pass; 161 | except 162 | on x: Exception do begin 163 | if mxInitialized then FinalizeMXPF; 164 | Fail(x); 165 | end; 166 | end; 167 | 168 | // All tests passed 169 | Pass; 170 | except 171 | on x: Exception do Fail(x); 172 | end; 173 | end; 174 | 175 | 176 | {******************************************************************************} 177 | { TEST PATCH FILE SELECTION 178 | Tests Patch File Selection MXPF functions: 179 | - PatchFileByAuthor 180 | - PatchFileByName 181 | } 182 | {******************************************************************************} 183 | 184 | procedure TestPatchFileSelection; 185 | var 186 | bCaughtException: boolean; 187 | begin 188 | Describe('Patch File Selection'); 189 | try 190 | Describe('PatchFileByAuthor'); 191 | try 192 | // Test MXPF not initialized 193 | Describe('MXPF not initialized'); 194 | try 195 | bCaughtException := false; 196 | try 197 | PatchFileByAuthor('MXPF Tests'); 198 | except 199 | on x: Exception do begin 200 | bCaughtException := true; 201 | ExpectEqual(x.Message, 'MXPF Error: You need to call InitializeMXPF before calling PatchFileByAuthor', 'Should raise the correct exception'); 202 | end; 203 | end; 204 | Expect(bCaughtException, 'Should have raised an exception'); 205 | Expect(not Assigned(mxPatchFile), 'Should not assign mxPatchFile'); 206 | Pass; 207 | except 208 | on x: Exception do Fail(x); 209 | end; 210 | 211 | // Test file loaded 212 | Describe('File loaded'); 213 | try 214 | InitializeMXPF; 215 | PatchFileByAuthor('MXPF Tests'); 216 | Expect(Assigned(mxPatchFile), 'Should find the file'); 217 | ExpectEqual(GetAuthor(mxPatchFile), 'MXPF Tests', 'Author should match'); 218 | FinalizeMXPF; 219 | Pass; 220 | except 221 | on x: Exception do begin 222 | if mxInitialized then FinalizeMXPF; 223 | Fail(x); 224 | end; 225 | end; 226 | 227 | // Test file not loaded 228 | Describe('File not loaded'); 229 | try 230 | InitializeMXPF; 231 | PatchFileByAuthor('SomeRandomAuthor'); 232 | Expect(true, 'Should not throw an exception'); 233 | Expect(not Assigned(mxPatchFile), 'Should not assign mxPatchFile'); 234 | FinalizeMXPF; 235 | Pass; 236 | except 237 | on x: Exception do begin 238 | if mxInitialized then FinalizeMXPF; 239 | Fail(x); 240 | end; 241 | end; 242 | 243 | // Test multiple matching files 244 | Describe('Multiple matching files'); 245 | try 246 | InitializeMXPF; 247 | PatchFileByAuthor('MXPF Tests'); 248 | ExpectEqual(GetFileName(mxPatchFile), 'TestMXPF-1.esp', 'Should find first file matching author'); 249 | FinalizeMXPF; 250 | Pass; 251 | except 252 | on x: Exception do begin 253 | if mxInitialized then FinalizeMXPF; 254 | Fail(x); 255 | end; 256 | end; 257 | 258 | // All tests passed? 259 | Pass; 260 | except 261 | on x: Exception do Fail(x); 262 | end; 263 | 264 | Describe('PatchFileByName'); 265 | try 266 | // Test MXPF not initialized 267 | Describe('MXPF not initialized'); 268 | try 269 | bCaughtException := false; 270 | try 271 | PatchFileByName('TestMXPF-1.esp'); 272 | except 273 | on x: Exception do begin 274 | bCaughtException := true; 275 | ExpectEqual(x.Message, 'MXPF Error: You need to call InitializeMXPF before calling PatchFileByName', 'Should raise the correct exception'); 276 | end; 277 | end; 278 | Expect(bCaughtException, 'Should have raised an exception'); 279 | Expect(not Assigned(mxPatchFile), 'Should not assign mxPatchFile'); 280 | Pass; 281 | except 282 | on x: Exception do Fail(x); 283 | end; 284 | 285 | // Test file loaded 286 | Describe('File loaded'); 287 | try 288 | InitializeMXPF; 289 | PatchFileByName('TestMXPF-1.esp'); 290 | Expect(Assigned(mxPatchFile), 'Should find the file'); 291 | ExpectEqual('TestMXPF-1.esp', GetFileName(mxPatchFile), 'Filenames should match'); 292 | FinalizeMXPF; 293 | Pass; 294 | except 295 | on x: Exception do begin 296 | if mxInitialized then FinalizeMXPF; 297 | Fail(x); 298 | end; 299 | end; 300 | 301 | // Test file not loaded 302 | Describe('File not loaded'); 303 | try 304 | InitializeMXPF; 305 | PatchFileByAuthor('SomeRandomFilename.esp'); 306 | Expect(true, 'Should not throw an exception'); 307 | Expect(not Assigned(mxPatchFile), 'Should not assign mxPatchFile'); 308 | FinalizeMXPF; 309 | Pass; 310 | except 311 | on x: Exception do begin 312 | if mxInitialized then FinalizeMXPF; 313 | Fail(x); 314 | end; 315 | end; 316 | 317 | // All tests passed? 318 | Pass; 319 | except 320 | on x: Exception do Fail(x); 321 | end; 322 | 323 | // All tests passed? 324 | Pass; 325 | except 326 | on x: Exception do Fail(x); 327 | end; 328 | end; 329 | 330 | 331 | {******************************************************************************} 332 | { TEST FILE SELECTION 333 | Tests File Selection MXPF functions: 334 | - SetExclusions 335 | - SetInclusions 336 | } 337 | {******************************************************************************} 338 | 339 | procedure TestFileSelection; 340 | var 341 | bCaughtException: boolean; 342 | begin 343 | Describe('File Selection'); 344 | try 345 | Describe('SetExclusions'); 346 | try 347 | // Test with MXPF not initialized 348 | Describe('MXPF not initialized'); 349 | try 350 | bCaughtException := false; 351 | try 352 | SetExclusions('TestMXPF-2.esp'); 353 | except 354 | on x: Exception do begin 355 | bCaughtException := true; 356 | ExpectEqual(x.Message, 'MXPF Error: You need to call InitializeMXPF before calling SetExclusions', 'Should raise the correction exception'); 357 | end; 358 | end; 359 | Expect(bCaughtException, 'Should have raised an exception'); 360 | ExpectEqual(mxFileMode, 0, 'Should not change mxFileMode'); 361 | Pass; 362 | except 363 | on x: Exception do Fail(x); 364 | end; 365 | 366 | // Test with MXPF initialized 367 | Describe('MXPF initialized'); 368 | try 369 | InitializeMXPF; 370 | SetExclusions('TestMXPF-2.esp'); 371 | ExpectEqual(mxFileMode, 1, 'Should set mxFileMode to 1'); 372 | ExpectEqual(mxFiles.Text, 'TestMXPF-2.esp'#13#10, 373 | 'Should load the specified files into mxFiles'); 374 | FinalizeMXPF; 375 | Pass; 376 | except 377 | on x: Exception do begin 378 | if mxInitialized then FinalizeMXPF; 379 | Fail(x); 380 | end; 381 | end; 382 | 383 | // All tests passed? 384 | Pass; 385 | except 386 | on x: Exception do Fail(x); 387 | end; 388 | 389 | Describe('SetInclusions'); 390 | try 391 | // Test with MXPF not initialized 392 | Describe('MXPF not initialized'); 393 | try 394 | bCaughtException := false; 395 | try 396 | SetInclusions('TestMXPF-1.esp,TestMXPF-2.esp'); 397 | except 398 | on x: Exception do begin 399 | bCaughtException := true; 400 | ExpectEqual(x.Message, 'MXPF Error: You need to call InitializeMXPF before calling SetInclusions', 'Should raise the correct exception'); 401 | end; 402 | end; 403 | Expect(bCaughtException, 'Should have raised an exception'); 404 | ExpectEqual(mxFileMode, 0, 'Should not change mxFileMode'); 405 | Pass; 406 | except 407 | on x: Exception do Fail(x); 408 | end; 409 | 410 | // Test with MXPF initialized 411 | Describe('MXPF initialized'); 412 | try 413 | InitializeMXPF; 414 | SetInclusions('TestMXPF-1.esp,TestMXPF-2.esp'); 415 | ExpectEqual(mxFileMode, 2, 'Should set mxFileMode to 2'); 416 | ExpectEqual(mxFiles.Text, 'TestMXPF-1.esp'#13#10'TestMXPF-2.esp'#13#10, 417 | 'Should load the specified files into mxFiles'); 418 | FinalizeMXPF; 419 | Pass; 420 | except 421 | on x: Exception do begin 422 | if mxInitialized then FinalizeMXPF; 423 | Fail(x); 424 | end; 425 | end; 426 | 427 | // All tests passed? 428 | Pass; 429 | except 430 | on x: Exception do Fail(x); 431 | end; 432 | 433 | Pass; 434 | except 435 | on x: Exception do Fail(x); 436 | end; 437 | end; 438 | 439 | 440 | {******************************************************************************} 441 | { TEST RECORD PROCESSING 442 | Tests Record Processing MXPF functions: 443 | - LoadRecords 444 | - LoadChildRecords 445 | - GetRecord 446 | - RemoveRecord 447 | - MaxRecordIndex 448 | } 449 | {******************************************************************************} 450 | 451 | procedure TestRecordProcessing; 452 | var 453 | bCaughtException: boolean; 454 | rec: IInterface; 455 | s: string; 456 | begin 457 | Describe('Record Processing'); 458 | try 459 | Describe('LoadRecords'); 460 | try 461 | // Test with MXPF not initialized 462 | Describe('MXPF not initialized'); 463 | try 464 | bCaughtException := false; 465 | try 466 | LoadRecords('ARMO'); 467 | except 468 | on x: Exception do begin 469 | bCaughtException := true; 470 | ExpectEqual(x.Message, 'MXPF Error: You need to call InitializeMXPF before calling LoadRecords', 'Should raise the correct exception'); 471 | end; 472 | end; 473 | Expect(bCaughtException, 'Should have raised an exception'); 474 | Pass; 475 | except 476 | on x: Exception do Fail(x); 477 | end; 478 | 479 | // Test with mxPatchFile not assigned 480 | Describe('Patch file not assigned'); 481 | try 482 | InitializeMXPF; 483 | LoadRecords('ARMO'); 484 | Expect(true, 'Should not throw an exception'); 485 | FinalizeMXPF; 486 | Pass; 487 | except 488 | on x: Exception do begin 489 | if mxInitialized then FinalizeMXPF; 490 | Fail(x); 491 | end; 492 | end; 493 | 494 | // Test with mxPatchFile assigned 495 | Describe('Patch file assigned'); 496 | try 497 | InitializeMXPF; 498 | PatchFileByAuthor('MXPF Tests'); 499 | LoadRecords('ASTP'); 500 | ExpectEqual(mxMasters.Text, 'Skyrim.esm'#13#10, 'Should load files into mxMasters'); 501 | ExpectEqual(mxRecords.Count, 20, 'Should load records into mxRecords'); 502 | FinalizeMXPF; 503 | Pass; 504 | except 505 | on x: Exception do begin 506 | if mxInitialized then FinalizeMXPF; 507 | Fail(x); 508 | end; 509 | end; 510 | 511 | // Test exclusion mode 512 | Describe('Exclusion mode'); 513 | try 514 | InitializeMXPF; 515 | PatchFileByName('TestMXPF-2.esp'); 516 | SetExclusions('TestMXPF-1.esp'); 517 | LoadRecords('ASTP'); 518 | ExpectEqual(mxMasters.Text, 'Skyrim.esm'#13#10, 'Should not add masters from skipped files'); 519 | ExpectEqual(mxRecords.Count, 20, 'Should not load records from excluded files'); 520 | FinalizeMXPF; 521 | Pass; 522 | except 523 | on x: Exception do begin 524 | if mxInitialized then FinalizeMXPF; 525 | Fail(x); 526 | end; 527 | end; 528 | 529 | // Test inclusion mode 530 | Describe('Inclusion mode'); 531 | try 532 | InitializeMXPF; 533 | PatchFileByName('TestMXPF-2.esp'); 534 | SetInclusions('Skyrim.esm'); 535 | LoadRecords('ASTP'); 536 | ExpectEqual(mxMasters.Text, 'Skyrim.esm'#13#10, 'Should only add masters from included files'); 537 | ExpectEqual(mxRecords.Count, 20, 'Should only load records from included files'); 538 | FinalizeMXPF; 539 | Pass; 540 | except 541 | on x: Exception do begin 542 | if mxInitialized then FinalizeMXPF; 543 | Fail(x); 544 | end; 545 | end; 546 | 547 | // Test mxLoadMasterRecords 548 | Describe('mxLoadMasterRecords'); 549 | try 550 | InitializeMXPF; 551 | mxLoadMasterRecords := true; 552 | PatchFileByName('TestMXPF-2.esp'); 553 | LoadRecords('ARMO'); 554 | ExpectEqual(mxRecords.Count, 2763, 'Should only load master records'); 555 | FinalizeMXPF; 556 | Pass; 557 | except 558 | on x: Exception do begin 559 | if mxInitialized then FinalizeMXPF; 560 | Fail(x); 561 | end; 562 | end; 563 | 564 | // Test mxLoadOverrideRecords 565 | Describe('mxLoadOverrideRecords'); 566 | try 567 | InitializeMXPF; 568 | mxLoadOverrideRecords := true; 569 | PatchFileByName('TestMXPF-2.esp'); 570 | LoadRecords('ARMO'); 571 | ExpectEqual(mxRecords.Count, 8, 'Should only load override records'); 572 | FinalizeMXPF; 573 | Pass; 574 | except 575 | on x: Exception do begin 576 | if mxInitialized then FinalizeMXPF; 577 | Fail(x); 578 | end; 579 | end; 580 | 581 | // Test mxLoadWinningOverrides 582 | Describe('mxLoadWinningOverrides'); 583 | try 584 | InitializeMXPF; 585 | mxLoadWinningOverrides := true; 586 | SetExclusions('Skyrim.esm'); 587 | PatchFileByName('TestMXPF-3.esp'); 588 | LoadRecords('ARMO'); 589 | ExpectEqual(mxRecords.Count, 26, 'Should load records'); 590 | rec := GetRecord(0); 591 | s := GetElementEditValues(rec, 'DNAM'); 592 | ExpectEqual(s, '15.000000', 'Should have the winning override record'); 593 | FinalizeMXPF; 594 | 595 | // when winning override is in a file not in the file selection 596 | InitializeMXPF; 597 | mxLoadWinningOverrides := true; 598 | SetExclusions('Skyrim.esm'); 599 | PatchFileByName('TestMXPF-2.esp'); 600 | LoadRecords('WEAP'); 601 | ExpectEqual(mxRecords.Count, 1, 'Should load records'); 602 | rec := GetRecord(0); 603 | s := GetElementEditValues(rec, 'DATA\Damage'); 604 | ExpectEqual(s, '8', 'Should not copy winning override from file outside of the file selection'); 605 | FinalizeMXPF; 606 | 607 | Pass; 608 | except 609 | on x: Exception do begin 610 | if mxInitialized then FinalizeMXPF; 611 | Fail(x); 612 | end; 613 | end; 614 | 615 | // Test group not found in file selection 616 | Describe('Empty group'); 617 | try 618 | InitializeMXPF; 619 | SetExclusions('Skyrim.esm'); 620 | LoadRecords('SLGM'); 621 | ExpectEqual(mxRecords.Count, 0, 'Should load 0 records'); 622 | FinalizeMXPF; 623 | Pass; 624 | except 625 | on x: Exception do begin 626 | if mxInitialized then FinalizeMXPF; 627 | Fail(x); 628 | end; 629 | end; 630 | 631 | // All tests passed? 632 | Pass; 633 | except 634 | on x: Exception do Fail(x); 635 | end; 636 | 637 | 638 | Describe('LoadChildRecords'); 639 | try 640 | // Test with MXPF not initialized 641 | Describe('MXPF not initialized'); 642 | try 643 | bCaughtException := false; 644 | try 645 | LoadChildRecords('CELL', 'REFR'); 646 | except 647 | on x: Exception do begin 648 | bCaughtException := true; 649 | ExpectEqual(x.Message, 'MXPF Error: You need to call InitializeMXPF before calling LoadChildRecords', 'Should raise the correct exception'); 650 | end; 651 | end; 652 | Expect(bCaughtException, 'Should have raised an exception'); 653 | Pass; 654 | except 655 | on x: Exception do Fail(x); 656 | end; 657 | 658 | // Test with mxPatchFile not assigned 659 | Describe('Patch file not assigned'); 660 | try 661 | InitializeMXPF; 662 | LoadChildRecords('CELL', 'REFR'); 663 | Expect(true, 'Should not throw an exception'); 664 | FinalizeMXPF; 665 | Pass; 666 | except 667 | on x: Exception do begin 668 | if mxInitialized then FinalizeMXPF; 669 | Fail(x); 670 | end; 671 | end; 672 | 673 | // Test with mxPatchFile assigned 674 | Describe('Patch file assigned'); 675 | try 676 | InitializeMXPF; 677 | PatchFileByAuthor('MXPF Tests'); 678 | LoadChildRecords('CELL', 'ACHR'); 679 | ExpectEqual(mxMasters.Text, 'Skyrim.esm'#13#10, 'Should load files into mxMasters'); 680 | ExpectEqual(mxRecords.Count, 4452, 'Should load records into mxRecords'); 681 | FinalizeMXPF; 682 | Pass; 683 | except 684 | on x: Exception do begin 685 | if mxInitialized then FinalizeMXPF; 686 | Fail(x); 687 | end; 688 | end; 689 | 690 | // Test exclusion mode 691 | Describe('Exclusion mode'); 692 | try 693 | InitializeMXPF; 694 | PatchFileByName('TestMXPF-2.esp'); 695 | SetExclusions('TestMXPF-1.esp'); 696 | LoadChildRecords('CELL', 'PGRE'); 697 | ExpectEqual(mxMasters.Text, 'Skyrim.esm'#13#10, 'Should not add masters from skipped files'); 698 | ExpectEqual(mxRecords.Count, 29, 'Should not load records from excluded files'); 699 | FinalizeMXPF; 700 | Pass; 701 | except 702 | on x: Exception do begin 703 | if mxInitialized then FinalizeMXPF; 704 | Fail(x); 705 | end; 706 | end; 707 | 708 | // Test inclusion mode 709 | Describe('Inclusion mode'); 710 | try 711 | InitializeMXPF; 712 | PatchFileByName('TestMXPF-2.esp'); 713 | SetInclusions('Skyrim.esm'); 714 | LoadChildRecords('CELL', 'PGRE'); 715 | ExpectEqual(mxMasters.Text, 'Skyrim.esm'#13#10, 'Should only add masters from included files'); 716 | ExpectEqual(mxRecords.Count, 29, 'Should only load records from included files'); 717 | FinalizeMXPF; 718 | Pass; 719 | except 720 | on x: Exception do begin 721 | if mxInitialized then FinalizeMXPF; 722 | Fail(x); 723 | end; 724 | end; 725 | 726 | // Test mxLoadMasterRecords 727 | Describe('mxLoadMasterRecords'); 728 | try 729 | InitializeMXPF; 730 | mxLoadMasterRecords := true; 731 | PatchFileByName('TestMXPF-2.esp'); 732 | LoadChildRecords('CELL', 'ACHR'); 733 | ExpectEqual(mxRecords.Count, 4452, 'Should only load master records'); 734 | FinalizeMXPF; 735 | Pass; 736 | except 737 | on x: Exception do begin 738 | if mxInitialized then FinalizeMXPF; 739 | Fail(x); 740 | end; 741 | end; 742 | 743 | // Test mxLoadOverrideRecords 744 | Describe('mxLoadOverrideRecords'); 745 | try 746 | InitializeMXPF; 747 | mxLoadOverrideRecords := true; 748 | PatchFileByName('TestMXPF-2.esp'); 749 | LoadChildRecords('CELL', 'ACHR'); 750 | ExpectEqual(mxRecords.Count, 8, 'Should only load override records'); 751 | FinalizeMXPF; 752 | Pass; 753 | except 754 | on x: Exception do begin 755 | if mxInitialized then FinalizeMXPF; 756 | Fail(x); 757 | end; 758 | end; 759 | 760 | // Test mxLoadWinningOverrides 761 | Describe('mxLoadWinningOverrides'); 762 | try 763 | InitializeMXPF; 764 | mxLoadWinningOverrides := true; 765 | SetExclusions('Skyrim.esm'); 766 | PatchFileByName('TestMXPF-3.esp'); 767 | LoadChildRecords('CELL', 'ACHR'); 768 | ExpectEqual(mxRecords.Count, 16, 'Should load records'); 769 | rec := GetRecord(0); 770 | s := GetElementEditValues(rec, 'DATA\Position\Z'); 771 | ExpectEqual(s, '242.000000', 'Should have the winning override record'); 772 | FinalizeMXPF; 773 | 774 | // when winning override is in a file not in the file selection 775 | InitializeMXPF; 776 | mxLoadWinningOverrides := true; 777 | SetExclusions('Skyrim.esm'); 778 | PatchFileByName('TestMXPF-2.esp'); 779 | LoadChildRecords('CELL', 'ACHR'); 780 | ExpectEqual(mxRecords.Count, 8, 'Should load records'); 781 | rec := GetRecord(0); 782 | s := GetElementEditValues(rec, 'DATA\Position\Z'); 783 | ExpectEqual(s, '241.000000', 'Should not use winning override from file outside of the file selection'); 784 | FinalizeMXPF; 785 | 786 | Pass; 787 | except 788 | on x: Exception do begin 789 | if mxInitialized then FinalizeMXPF; 790 | Fail(x); 791 | end; 792 | end; 793 | 794 | // Test record signature not found in file selection 795 | Describe('No records matching signature'); 796 | try 797 | InitializeMXPF; 798 | SetExclusions('Skyrim.esm'); 799 | LoadChildRecords('CELL', 'NAVM'); 800 | ExpectEqual(mxRecords.Count, 0, 'Should load 0 records'); 801 | FinalizeMXPF; 802 | Pass; 803 | except 804 | on x: Exception do begin 805 | if mxInitialized then FinalizeMXPF; 806 | Fail(x); 807 | end; 808 | end; 809 | 810 | // All tests passed? 811 | Pass; 812 | except 813 | on x: Exception do Fail(x); 814 | end; 815 | 816 | Describe('MaxRecordIndex'); 817 | try 818 | // Test with MXPF not initialized 819 | Describe('MXPF not initialized'); 820 | try 821 | bCaughtException := false; 822 | try 823 | MaxRecordIndex; 824 | except 825 | on x: Exception do begin 826 | bCaughtException := true; 827 | ExpectEqual(x.Message, 'MXPF Error: You need to call InitialzeMXPF before calling MaxRecordIndex', 'Should raise the correct exception'); 828 | end; 829 | end; 830 | Expect(bCaughtException, 'Should have raised an exception'); 831 | Pass; 832 | except 833 | on x: Exception do Fail(x); 834 | end; 835 | 836 | // Test with MXPF initialized 837 | Describe('MXPF initialized'); 838 | try 839 | InitializeMXPF; 840 | PatchFileByName('TestMXPF-2.esp'); 841 | LoadRecords('ARMO'); 842 | ExpectEqual(MaxRecordIndex, mxRecords.Count - 1, 'Should return mxRecords.Count - 1'); 843 | FinalizeMXPF; 844 | Pass; 845 | except 846 | on x: Exception do begin 847 | if mxInitialized then FinalizeMXPF; 848 | Fail(x); 849 | end; 850 | end; 851 | 852 | // All tests passed? 853 | Pass; 854 | except 855 | on x: Exception do Fail(x); 856 | end; 857 | 858 | Describe('GetRecord'); 859 | try 860 | // Test with MXPF not initialized 861 | Describe('MXPF not initialized'); 862 | try 863 | bCaughtException := false; 864 | try 865 | rec := GetRecord(0); 866 | except 867 | on x: Exception do begin 868 | bCaughtException := true; 869 | ExpectEqual(x.Message, 'MXPF Error: You need to call InitialzeMXPF before calling LoadRecords', 'Should raise the correct exception'); 870 | end; 871 | end; 872 | Expect(bCaughtException, 'Should have raised an exception'); 873 | Pass; 874 | except 875 | on x: Exception do Fail(x); 876 | end; 877 | 878 | // Test with LoadRecords not called 879 | Describe('LoadRecords not called'); 880 | try 881 | bCaughtException := false; 882 | try 883 | InitializeMXPF; 884 | GetRecord(0); 885 | except 886 | on x: Exception do begin 887 | bCaughtException := true; 888 | ExpectEqual(x.Message, 'MXPF Error: You need to call LoadRecords before you can access records using GetRecord', 'Should raise the correct exception'); 889 | end; 890 | end; 891 | Expect(bCaughtException, 'Should have raised an exception'); 892 | FinalizeMXPF; 893 | Pass; 894 | except 895 | on x: Exception do begin 896 | if mxInitialized then FinalizeMXPF; 897 | Fail(x); 898 | end; 899 | end; 900 | 901 | // Test with no records loaded 902 | Describe('No records loaded'); 903 | try 904 | bCaughtException := false; 905 | try 906 | InitializeMXPF; 907 | SetExclusions('Skyrim.esm'); 908 | LoadRecords('SLGM'); 909 | GetRecord(0); 910 | except 911 | on x: Exception do begin 912 | bCaughtException := true; 913 | ExpectEqual(x.Message, 'MXPF Error: Can''t call GetRecord, no records available', 'Should raise the correct exception'); 914 | end; 915 | end; 916 | Expect(bCaughtException, 'Should have raised an exception'); 917 | FinalizeMXPF; 918 | Pass; 919 | except 920 | on x: Exception do begin 921 | if mxInitialized then FinalizeMXPF; 922 | Fail(x); 923 | end; 924 | end; 925 | 926 | // Test with an invalid index 927 | Describe('Invalid index'); 928 | try 929 | try 930 | InitializeMXPF; 931 | SetExclusions('Skyrim.esm'); 932 | LoadRecords('ARMO'); 933 | GetRecord(-1); 934 | except 935 | on x: Exception do begin 936 | bCaughtException := true; 937 | ExpectEqual(x.Message, 'MXPF Error: GetRecord index out of bounds', 938 | 'Should raise the correct exception'); 939 | end; 940 | end; 941 | // if no exception caught, fail test 942 | Expect(bCaughtException, 'Should have raised an exception'); 943 | FinalizeMXPF; 944 | Pass; 945 | except 946 | on x: Exception do begin 947 | if mxInitialized then FinalizeMXPF; 948 | Fail(x); 949 | end; 950 | end; 951 | 952 | // Test with valid index 953 | Describe('Valid index'); 954 | try 955 | InitializeMXPF; 956 | SetExclusions('Skyrim.esm'); 957 | PatchFileByName('TestMXPF-2.esp'); 958 | LoadRecords('ASTP'); 959 | Expect(Assigned(GetRecord(0)), 'Should return a record'); 960 | ExpectEqual(Name(GetRecord(0)), 'Friend [ASTP:01000800]', 'Should return the corect record'); 961 | FinalizeMXPF; 962 | Pass; 963 | except 964 | on x: Exception do begin 965 | if mxInitialized then FinalizeMXPF; 966 | Fail(x); 967 | end; 968 | end; 969 | 970 | // All tests passed? 971 | Pass; 972 | except 973 | on x: Exception do Fail(x); 974 | end; 975 | 976 | Describe('RemoveRecord'); 977 | try 978 | // Test with MXPF not initialized 979 | Describe('MXPF not initialized'); 980 | try 981 | bCaughtException := false; 982 | try 983 | RemoveRecord(0); 984 | except 985 | on x: Exception do begin 986 | bCaughtException := true; 987 | ExpectEqual(x.Message, 'MXPF Error: You need to call InitialzeMXPF before calling RemoveRecord', 'Should raise the correct exception'); 988 | end; 989 | end; 990 | Expect(bCaughtException, 'Should have raised an exception'); 991 | Pass; 992 | except 993 | on x: Exception do Fail(x); 994 | end; 995 | 996 | // Test with LoadRecords not called 997 | Describe('LoadRecords not called'); 998 | try 999 | bCaughtException := false; 1000 | try 1001 | InitializeMXPF; 1002 | RemoveRecord(0); 1003 | except 1004 | on x: Exception do begin 1005 | bCaughtException := true; 1006 | ExpectEqual(x.Message, 'MXPF Error: You need to call LoadRecords before you can remove records using RemoveRecord', 'Should raise the correct exception'); 1007 | end; 1008 | end; 1009 | Expect(bCaughtException, 'Should have raised an exception'); 1010 | FinalizeMXPF; 1011 | Pass; 1012 | except 1013 | on x: Exception do begin 1014 | if mxInitialized then FinalizeMXPF; 1015 | Fail(x); 1016 | end; 1017 | end; 1018 | 1019 | // Test with no records loaded 1020 | Describe('No records loaded'); 1021 | try 1022 | bCaughtException := false; 1023 | try 1024 | InitializeMXPF; 1025 | SetExclusions('Skyrim.esm'); 1026 | LoadRecords('SLGM'); 1027 | RemoveRecord(0); 1028 | except 1029 | on x: Exception do begin 1030 | bCaughtException := true; 1031 | ExpectEqual(x.Message, 'MXPF Error: Can''t call RemoveRecord, no records available', 'Should raise the correct exception'); 1032 | end; 1033 | end; 1034 | Expect(bCaughtException, 'Should have raised an exception'); 1035 | FinalizeMXPF; 1036 | Pass; 1037 | except 1038 | on x: Exception do begin 1039 | if mxInitialized then FinalizeMXPF; 1040 | Fail(x); 1041 | end; 1042 | end; 1043 | 1044 | // Test with an invalid index 1045 | Describe('Invalid index'); 1046 | try 1047 | try 1048 | InitializeMXPF; 1049 | SetExclusions('Skyrim.esm'); 1050 | LoadRecords('ARMO'); 1051 | RemoveRecord(-1); 1052 | except 1053 | on x: Exception do begin 1054 | bCaughtException := true; 1055 | ExpectEqual(x.Message, 'MXPF Error: RemoveRecord index out of bounds', 1056 | 'Should raise the correct exception'); 1057 | end; 1058 | end; 1059 | Expect(bCaughtException, 'Should have raised an exception'); 1060 | FinalizeMXPF; 1061 | Pass; 1062 | except 1063 | on x: Exception do begin 1064 | if mxInitialized then FinalizeMXPF; 1065 | Fail(x); 1066 | end; 1067 | end; 1068 | 1069 | // Test with valid index 1070 | Describe('Valid index'); 1071 | try 1072 | InitializeMXPF; 1073 | SetExclusions('Skyrim.esm'); 1074 | PatchFileByName('TestMXPF-2.esp'); 1075 | LoadRecords('ASTP'); 1076 | RemoveRecord(0); 1077 | ExpectEqual(mxRecords.Count, 0, 'Should remove the record at the index'); 1078 | FinalizeMXPF; 1079 | Pass; 1080 | except 1081 | on x: Exception do begin 1082 | if mxInitialized then FinalizeMXPF; 1083 | Fail(x); 1084 | end; 1085 | end; 1086 | 1087 | // All tests passed? 1088 | Pass; 1089 | except 1090 | on x: Exception do Fail(x); 1091 | end; 1092 | 1093 | // All tests passed? 1094 | Pass; 1095 | except 1096 | on x: Exception do Fail(x); 1097 | end; 1098 | end; 1099 | 1100 | 1101 | {******************************************************************************} 1102 | { TEST RECORD PATCHING 1103 | Tests Record Patching MXPF functions: 1104 | - AddMastersToPatch 1105 | - CopyRecordToPatch 1106 | - CopyRecordsToPatch 1107 | - MaxPatchRecordIndex 1108 | - GetPatchRecord 1109 | } 1110 | {******************************************************************************} 1111 | 1112 | procedure TestRecordPatching; 1113 | var 1114 | bCaughtException: boolean; 1115 | rec, g: IInterface; 1116 | s, fn: string; 1117 | sl: TStringList; 1118 | count: Integer; 1119 | begin 1120 | Describe('Record Patching'); 1121 | try 1122 | Describe('AddMastersToPatch'); 1123 | try 1124 | // Test with MXPF not initialized 1125 | Describe('MXPF not initialized'); 1126 | try 1127 | bCaughtException := false; 1128 | try 1129 | AddMastersToPatch; 1130 | except 1131 | on x: Exception do begin 1132 | bCaughtException := true; 1133 | ExpectEqual(x.Message, 'MXPF Error: You need to call InitialzeMXPF before calling AddMastersToPatch', 'Should raise the correct exception'); 1134 | end; 1135 | end; 1136 | Expect(bCaughtException, 'Should have raised an exception'); 1137 | Expect(not mxMastersAdded, 'Should not set mxMastersAdded to true'); 1138 | Pass; 1139 | except 1140 | on x: Exception do Fail(x); 1141 | end; 1142 | 1143 | // Test with LoadRecords not called 1144 | Describe('LoadRecords not called'); 1145 | try 1146 | bCaughtException := false; 1147 | try 1148 | InitializeMXPF; 1149 | AddMastersToPatch; 1150 | except 1151 | on x: Exception do begin 1152 | bCaughtException := true; 1153 | ExpectEqual(x.Message, 'MXPF Error: You need to call LoadRecords before you can call AddMastersToPatch', 'Should raise the correct exception'); 1154 | end; 1155 | end; 1156 | Expect(bCaughtException, 'Should have raised an exception'); 1157 | Expect(not mxMastersAdded, 'Should not set mxMastersAdded to true'); 1158 | FinalizeMXPF; 1159 | Pass; 1160 | except 1161 | on x: Exception do begin 1162 | if mxInitialized then FinalizeMXPF; 1163 | Fail(x); 1164 | end; 1165 | end; 1166 | 1167 | // Test with mxPatchFile not assigned 1168 | Describe('mxPatchFile not assigned'); 1169 | try 1170 | bCaughtException := false; 1171 | try 1172 | InitializeMXPF; 1173 | SetExclusions('Skyrim.esm'); 1174 | LoadRecords('ARMO'); 1175 | AddMastersToPatch; 1176 | except 1177 | on x: Exception do begin 1178 | bCaughtException := true; 1179 | ExpectEqual(x.Message, 'MXPF Error: You need to assign mxPatchFile using PatchFileByAuthor or PatchFileByName before calling AddMastersToPatch', 'Should raise the correct exception'); 1180 | end; 1181 | end; 1182 | Expect(bCaughtException, 'Should have raised an exception'); 1183 | Expect(not mxMastersAdded, 'Should not set mxMastersAdded to true'); 1184 | FinalizeMXPF; 1185 | Pass; 1186 | except 1187 | on x: Exception do begin 1188 | if mxInitialized then FinalizeMXPF; 1189 | Fail(x); 1190 | end; 1191 | end; 1192 | 1193 | // Test with mxPatchFile assigned 1194 | sl := TStringList.Create; 1195 | Describe('mxPatchFile assigned'); 1196 | try 1197 | InitializeMXPF; 1198 | PatchFileByName('TestMXPF-3.esp'); 1199 | LoadRecords('ARMO'); 1200 | AddMastersToPatch; 1201 | Expect(mxMastersAdded, 'Should set mxMastersAdded to true'); 1202 | GetMasters(mxPatchFile, sl); 1203 | ExpectEqual(sl.Text, 'Skyrim.esm'#13#10'TestMXPF-1.esp'#13#10'TestMXPF-2.esp'#13#10, 'Should add the correct masters'); 1204 | FinalizeMXPF; 1205 | Pass; 1206 | except 1207 | on x: Exception do begin 1208 | if mxInitialized then FinalizeMXPF; 1209 | Fail(x); 1210 | end; 1211 | end; 1212 | sl.Free; 1213 | 1214 | // All tests passed? 1215 | Pass; 1216 | except 1217 | on x: Exception do Fail(x); 1218 | end; 1219 | 1220 | Describe('CopyRecordToPatch'); 1221 | try 1222 | // Test with MXPF not initialized 1223 | Describe('MXPF not initialized'); 1224 | try 1225 | bCaughtException := false; 1226 | try 1227 | CopyRecordToPatch(0); 1228 | except 1229 | on x: Exception do begin 1230 | bCaughtException := true; 1231 | ExpectEqual(x.Message, 'MXPF Error: You need to call InitialzeMXPF before calling CopyRecordToPatch', 'Should raise the correct exception'); 1232 | end; 1233 | end; 1234 | Expect(bCaughtException, 'Should have raised an exception'); 1235 | Expect(not mxCopyCalled, 'Should not set mxCopyCalled to true'); 1236 | Pass; 1237 | except 1238 | on x: Exception do Fail(x); 1239 | end; 1240 | 1241 | // Test with LoadRecords not called 1242 | Describe('LoadRecords not called'); 1243 | try 1244 | bCaughtException := false; 1245 | try 1246 | InitializeMXPF; 1247 | CopyRecordToPatch(0); 1248 | except 1249 | on x: Exception do begin 1250 | bCaughtException := true; 1251 | ExpectEqual(x.Message, 'MXPF Error: You need to call LoadRecords before you can copy records using CopyRecordToPatch', 'Should raise the correct exception'); 1252 | end; 1253 | end; 1254 | Expect(bCaughtException, 'Should have raised an exception'); 1255 | Expect(not mxCopyCalled, 'Should not set mxCopyCalled to true'); 1256 | FinalizeMXPF; 1257 | Pass; 1258 | except 1259 | on x: Exception do begin 1260 | if mxInitialized then FinalizeMXPF; 1261 | Fail(x); 1262 | end; 1263 | end; 1264 | 1265 | // Test with mxPatchFile not assigned 1266 | Describe('mxPatchFile not assigned'); 1267 | try 1268 | bCaughtException := false; 1269 | try 1270 | InitializeMXPF; 1271 | SetExclusions('Skyrim.esm'); 1272 | LoadRecords('ARMO'); 1273 | CopyRecordToPatch(0); 1274 | except 1275 | on x: Exception do begin 1276 | bCaughtException := true; 1277 | ExpectEqual(x.Message, 'MXPF Error: You need to assign mxPatchFile using PatchFileByAuthor or PatchFileByName before calling CopyRecordToPatch', 'Should raise the correct exception'); 1278 | end; 1279 | end; 1280 | Expect(bCaughtException, 'Should have raised an exception'); 1281 | Expect(not mxCopyCalled, 'Should not set mxCopyCalled to true'); 1282 | FinalizeMXPF; 1283 | Pass; 1284 | except 1285 | on x: Exception do begin 1286 | if mxInitialized then FinalizeMXPF; 1287 | Fail(x); 1288 | end; 1289 | end; 1290 | 1291 | // Test with no records available 1292 | Describe('No records available'); 1293 | try 1294 | bCaughtException := false; 1295 | try 1296 | InitializeMXPF; 1297 | PatchFileByName('TestMXPF-3.esp'); 1298 | SetExclusions('Skyrim.esm'); 1299 | LoadRecords('SLGM'); 1300 | CopyRecordToPatch(0); 1301 | except 1302 | on x: Exception do begin 1303 | bCaughtException := true; 1304 | ExpectEqual(x.Message, 'MXPF Error: Can''t call CopyRecordToPatch, no records available', 'Should raise the correct exception'); 1305 | end; 1306 | end; 1307 | Expect(bCaughtException, 'Should have raised an exception'); 1308 | Expect(not mxCopyCalled, 'Should not set mxCopyCalled to true'); 1309 | FinalizeMXPF; 1310 | Pass; 1311 | except 1312 | on x: Exception do begin 1313 | if mxInitialized then FinalizeMXPF; 1314 | Fail(x); 1315 | end; 1316 | end; 1317 | 1318 | // Test invalid index 1319 | Describe('Invalid index'); 1320 | try 1321 | bCaughtException := false; 1322 | try 1323 | InitializeMXPF; 1324 | SetExclusions('Skyrim.esm'); 1325 | PatchFileByName('TestMXPF-3.esp'); 1326 | LoadRecords('ARMO'); 1327 | CopyRecordToPatch(-1); 1328 | except 1329 | on x: Exception do begin 1330 | bCaughtException := true; 1331 | ExpectEqual(x.Message, 'MXPF Error: CopyRecordToPatch index out of bounds', 'Should raise the correct exception'); 1332 | end; 1333 | end; 1334 | Expect(bCaughtException, 'Should have raised an exception'); 1335 | Expect(not mxCopyCalled, 'Should not set mxCopyCalled to true'); 1336 | FinalizeMXPF; 1337 | Pass; 1338 | except 1339 | on x: Exception do begin 1340 | if mxInitialized then FinalizeMXPF; 1341 | Fail(x); 1342 | end; 1343 | end; 1344 | 1345 | // Test valid index 1346 | Describe('Valid index'); 1347 | try 1348 | InitializeMXPF; 1349 | SetExclusions('Skyrim.esm'); 1350 | PatchFileByName('TestMXPF-3.esp'); 1351 | LoadRecords('ARMO'); 1352 | rec := CopyRecordToPatch(0); 1353 | s := Name(rec); 1354 | Remove(rec); 1355 | Expect(mxCopyCalled, 'Should set mxCopyCalled to true'); 1356 | Expect(mxMastersAdded, 'Should set mxMastersAdded to true'); 1357 | ExpectEqual(mxPatchRecords.Count, 1, 'Should add the record to mxPatchRecords'); 1358 | ExpectEqual(s, 'ArmorIronGauntlets "Iron Gauntlets" [ARMO:00012E46]', 'Record should be present in the patch file'); 1359 | FinalizeMXPF; 1360 | Pass; 1361 | except 1362 | on x: Exception do begin 1363 | if mxInitialized then FinalizeMXPF; 1364 | Fail(x); 1365 | end; 1366 | end; 1367 | 1368 | // Test mxSkipPatchedRecords 1369 | Describe('mxSkipPatchedRecords'); 1370 | try 1371 | InitializeMXPF; 1372 | mxSkipPatchedRecords := true; 1373 | SetExclusions('Skyrim.esm'); 1374 | PatchFileByName('TestMXPF-3.esp'); 1375 | LoadRecords('WEAP'); 1376 | Expect(not mxCopyCalled, 'Should not set mxCopyCalled to true'); 1377 | Expect(not mxMastersAdded, 'Should not set mxMastersAdded to true'); 1378 | ExpectEqual(mxPatchRecords.Count, 0, 'Should not add record to mxPatchRecords'); 1379 | Expect(not Assigned(CopyRecordToPatch(0)), 'Should not return a record'); 1380 | FinalizeMXPF; 1381 | Pass; 1382 | except 1383 | on x: Exception do begin 1384 | if mxInitialized then FinalizeMXPF; 1385 | Fail(x); 1386 | end; 1387 | end; 1388 | 1389 | // All tests passed? 1390 | Pass; 1391 | except 1392 | on x: Exception do Fail(x); 1393 | end; 1394 | 1395 | Describe('CopyRecordsToPatch'); 1396 | try 1397 | // Test with MXPF not initialized 1398 | Describe('MXPF not initialized'); 1399 | try 1400 | bCaughtException := false; 1401 | try 1402 | CopyRecordsToPatch; 1403 | except 1404 | on x: Exception do begin 1405 | bCaughtException := true; 1406 | ExpectEqual(x.Message, 'MXPF Error: You need to call InitialzeMXPF before calling CopyRecordsToPatch', 'Should raise the correct exception'); 1407 | end; 1408 | end; 1409 | Expect(bCaughtException, 'Should have raised an exception'); 1410 | Expect(not mxCopyCalled, 'Should not set mxCopyCalled to true'); 1411 | Pass; 1412 | except 1413 | on x: Exception do Fail(x); 1414 | end; 1415 | 1416 | // Test with LoadRecords not called 1417 | Describe('LoadRecords not called'); 1418 | try 1419 | bCaughtException := false; 1420 | try 1421 | InitializeMXPF; 1422 | CopyRecordsToPatch; 1423 | except 1424 | on x: Exception do begin 1425 | bCaughtException := true; 1426 | ExpectEqual(x.Message, 'MXPF Error: You need to call LoadRecords before you can copy records using CopyRecordsToPatch', 'Should raise the correct exception'); 1427 | end; 1428 | end; 1429 | Expect(bCaughtException, 'Should have raised an exception'); 1430 | Expect(not mxCopyCalled, 'Should not set mxCopyCalled to true'); 1431 | FinalizeMXPF; 1432 | Pass; 1433 | except 1434 | on x: Exception do Fail(x); 1435 | end; 1436 | 1437 | // Test with mxPatchFile not assigned 1438 | Describe('mxPatchFile not assigned'); 1439 | try 1440 | bCaughtException := false; 1441 | try 1442 | InitializeMXPF; 1443 | LoadRecords('ARMO'); 1444 | CopyRecordsToPatch; 1445 | except 1446 | on x: Exception do begin 1447 | bCaughtException := true; 1448 | ExpectEqual(x.Message, 'MXPF Error: You need to assign mxPatchFile using PatchFileByAuthor or PatchFileByName before calling CopyRecordsToPatch', 'Should raise the correct exception'); 1449 | end; 1450 | end; 1451 | Expect(bCaughtException, 'Should have raised an exception'); 1452 | Expect(not mxCopyCalled, 'Should not set mxCopyCalled to true'); 1453 | FinalizeMXPF; 1454 | Pass; 1455 | except 1456 | on x: Exception do begin 1457 | if mxInitialized then FinalizeMXPF; 1458 | Fail(x); 1459 | end; 1460 | end; 1461 | 1462 | // Test with no records 1463 | Describe('No records'); 1464 | try 1465 | bCaughtException := false; 1466 | try 1467 | InitializeMXPF; 1468 | SetExclusions('Skyrim.esm'); 1469 | PatchFileByName('TestMXPF-2.esp'); 1470 | LoadRecords('SLGM'); 1471 | CopyRecordsToPatch; 1472 | except 1473 | on x: Exception do begin 1474 | bCaughtException := true; 1475 | ExpectEqual(x.Message, 'MXPF Error: Can''t call CopyRecordsToPatch, no records available', 'Should raise the correct exception'); 1476 | end; 1477 | end; 1478 | Expect(bCaughtException, 'Should have raised an exception'); 1479 | Expect(not mxCopyCalled, 'Should not set mxCopyCalled to true'); 1480 | FinalizeMXPF; 1481 | Pass; 1482 | except 1483 | on x: Exception do begin 1484 | if mxInitialized then FinalizeMXPF; 1485 | Fail(x); 1486 | end; 1487 | end; 1488 | 1489 | // Test with records available 1490 | Describe('mxSkipPatchedRecords'); 1491 | try 1492 | InitializeMXPF; 1493 | mxSkipPatchedRecords := true; 1494 | SetExclusions('Skyrim.esm'); 1495 | PatchFileByName('TestMXPF-3.esp'); 1496 | LoadRecords('ARMO'); 1497 | CopyRecordsToPatch; 1498 | g := GroupBySignature(mxPatchFile, 'ARMO'); 1499 | count := ElementCount(g); 1500 | Remove(g); 1501 | Expect(mxCopyCalled, 'Should set mxCopyCalled to true'); 1502 | Expect(mxMastersAdded, 'Should set mxMastersAdded to true'); 1503 | ExpectEqual(mxPatchRecords.Count, 21, 'Should add the records to mxPatchRecords'); 1504 | ExpectEqual(count, 21, 'Should copy the records to the patch file'); 1505 | FinalizeMXPF; 1506 | Pass; 1507 | except 1508 | on x: Exception do begin 1509 | if mxInitialized then FinalizeMXPF; 1510 | Fail(x); 1511 | end; 1512 | end; 1513 | 1514 | // all tests passed? 1515 | Pass; 1516 | except 1517 | on x: Exception do Fail(x); 1518 | end; 1519 | 1520 | Describe('MaxPatchRecordIndex'); 1521 | try 1522 | // Test with MXPF not initialized 1523 | Describe('MXPF not initialized'); 1524 | try 1525 | bCaughtException := false; 1526 | try 1527 | MaxPatchRecordIndex; 1528 | except 1529 | on x: Exception do begin 1530 | bCaughtException := true; 1531 | ExpectEqual(x.Message, 'MXPF Error: You need to call InitialzeMXPF before calling MaxPatchRecordIndex', 'Should raise the correct exception.'); 1532 | end; 1533 | end; 1534 | Expect(bCaughtException, 'Should have raised an exception'); 1535 | Pass; 1536 | except 1537 | on x: Exception do Fail(x); 1538 | end; 1539 | 1540 | // Test with MXPF initialized 1541 | Describe('MXPF initialized'); 1542 | try 1543 | InitializeMXPF; 1544 | mxSkipPatchedRecords := true; 1545 | SetExclusions('Skyrim.esm'); 1546 | PatchFileByName('TestMXPF-3.esp'); 1547 | LoadRecords('ARMO'); 1548 | CopyRecordsToPatch; 1549 | g := GroupBySignature(mxPatchFile, 'ARMO'); 1550 | Remove(g); 1551 | ExpectEqual(MaxPatchRecordIndex, mxPatchRecords.Count - 1, 'Should return mxPatchRecords.Count - 1'); 1552 | FinalizeMXPF; 1553 | Pass; 1554 | except 1555 | on x: Exception do begin 1556 | if mxInitialized then FinalizeMXPF; 1557 | Fail(x); 1558 | end; 1559 | end; 1560 | 1561 | // All tests passed? 1562 | Pass; 1563 | except 1564 | on x: Exception do Fail(x); 1565 | end; 1566 | 1567 | Describe('GetPatchRecord'); 1568 | try 1569 | // Test with MXPF not initialized 1570 | Describe('MXPF not initialized'); 1571 | try 1572 | bCaughtException := false; 1573 | try 1574 | GetPatchRecord(0); 1575 | except 1576 | on x: Exception do begin 1577 | bCaughtException := true; 1578 | ExpectEqual(x.Message, 'MXPF Error: You need to call InitialzeMXPF before calling GetPatchRecord', 'Should raise the correct exception.'); 1579 | end; 1580 | end; 1581 | Expect(bCaughtException, 'Should have raised an exception'); 1582 | Pass; 1583 | except 1584 | on x: Exception do Fail(x); 1585 | end; 1586 | 1587 | // Test with CopyRecord(s)ToPatch not called 1588 | Describe('CopyRecord(s)ToPatch not called'); 1589 | try 1590 | bCaughtException := false; 1591 | try 1592 | InitializeMXPF; 1593 | mxSkipPatchedRecords := true; 1594 | SetExclusions('Skyrim.esm'); 1595 | PatchFileByName('TestMXPF-3.esp'); 1596 | LoadRecords('ARMO'); 1597 | GetPatchRecord(0); 1598 | except 1599 | on x : Exception do begin 1600 | bCaughtException := true; 1601 | ExpectEqual(x.Message, 'MXPF Error: You need to call CopyRecordsToPatch or CopyRecordToPatch before you can access records using GetPatchRecord', 'Should raise the correct exception.'); 1602 | end; 1603 | end; 1604 | Expect(bCaughtException, 'Should have raised an exception'); 1605 | FinalizeMXPF; 1606 | Pass; 1607 | except 1608 | on x: Exception do begin 1609 | if mxInitialized then FinalizeMXPF; 1610 | Fail(x); 1611 | end; 1612 | end; 1613 | 1614 | // Test with an invalid index 1615 | Describe('Invalid index'); 1616 | try 1617 | bCaughtException := false; 1618 | try 1619 | InitializeMXPF; 1620 | mxSkipPatchedRecords := true; 1621 | SetExclusions('Skyrim.esm'); 1622 | PatchFileByName('TestMXPF-3.esp'); 1623 | LoadRecords('ARMO'); 1624 | CopyRecordsToPatch; 1625 | GetPatchRecord(-1); 1626 | except 1627 | on x : Exception do begin 1628 | bCaughtException := true; 1629 | ExpectEqual(x.Message, 'MXPF Error: GetPatchRecord index out of bounds', 'Should raise the correct exception.'); 1630 | end; 1631 | end; 1632 | g := GroupBySignature(mxPatchFile, 'ARMO'); 1633 | Remove(g); 1634 | Expect(bCaughtException, 'Should have raised an exception'); 1635 | FinalizeMXPF; 1636 | Pass; 1637 | except 1638 | on x: Exception do begin 1639 | if mxInitialized then FinalizeMXPF; 1640 | Fail(x); 1641 | end; 1642 | end; 1643 | 1644 | // Test with a valid index 1645 | Describe('Valid index'); 1646 | try 1647 | InitializeMXPF; 1648 | mxSkipPatchedRecords := true; 1649 | SetExclusions('Skyrim.esm'); 1650 | PatchFileByName('TestMXPF-3.esp'); 1651 | LoadRecords('ARMO'); 1652 | CopyRecordsToPatch; 1653 | rec := GetPatchRecord(0); 1654 | s := Name(rec); 1655 | fn := GetFileName(GetFile(rec)); 1656 | g := GroupBySignature(mxPatchFile, 'ARMO'); 1657 | Remove(g); 1658 | ExpectEqual(s, 'ArmorIronGauntlets "Iron Gauntlets" [ARMO:00012E46]', 'Should return the correct record'); 1659 | ExpectEqual(fn, 'TestMXPF-3.esp', 'Should return record from the patch file.'); 1660 | FinalizeMXPF; 1661 | Pass; 1662 | except 1663 | on x: Exception do begin 1664 | if mxInitialized then FinalizeMXPF; 1665 | Fail(x); 1666 | end; 1667 | end; 1668 | 1669 | // All tests passed? 1670 | Pass; 1671 | except 1672 | on x: Exception do Fail(x); 1673 | end; 1674 | 1675 | 1676 | // All tests passed? 1677 | Pass; 1678 | except 1679 | on x: Exception do Fail(x); 1680 | end; 1681 | end; 1682 | 1683 | 1684 | {******************************************************************************} 1685 | { TEST MACROS 1686 | Tests MXPF Macro functions 1687 | - 1688 | } 1689 | {******************************************************************************} 1690 | 1691 | procedure TestMacros; 1692 | var 1693 | bCaughtException: boolean; 1694 | rec, g: IInterface; 1695 | s, fn: string; 1696 | sl: TStringList; 1697 | count: Integer; 1698 | begin 1699 | Describe('Macros'); 1700 | try 1701 | Describe('MultiLoad'); 1702 | try 1703 | Describe('Input records string is empty'); 1704 | try 1705 | InitializeMXPF; 1706 | DefaultOptionsMXPF; 1707 | SetInclusions('Skyrim.esm'); 1708 | MultiLoad(''); 1709 | Expect(not mxLoadCalled, 'Should not load any records'); 1710 | FinalizeMXPF; 1711 | Pass; 1712 | except 1713 | on x: Exception do begin 1714 | if mxInitialized then FinalizeMXPF; 1715 | Fail(x); 1716 | end; 1717 | end; 1718 | 1719 | Describe('Single record signature input'); 1720 | try 1721 | InitializeMXPF; 1722 | DefaultOptionsMXPF; 1723 | SetInclusions('Skyrim.esm'); 1724 | MultiLoad('ARMO'); 1725 | Expect(mxLoadCalled, 'Should load records'); 1726 | ExpectEqual(mxRecords.Count, 2762, 'Should load the correct records'); 1727 | FinalizeMXPF; 1728 | Pass; 1729 | except 1730 | on x: Exception do begin 1731 | if mxInitialized then FinalizeMXPF; 1732 | Fail(x); 1733 | end; 1734 | end; 1735 | 1736 | Describe('Single signature pair input'); 1737 | try 1738 | InitializeMXPF; 1739 | SetExclusions(mxBethesdaSkyrimFiles); 1740 | MultiLoad('CELL:ACHR'); 1741 | Expect(mxLoadCalled, 'Should load records'); 1742 | ExpectEqual(mxRecords.Count, 24, 'Should load the correct records'); 1743 | FinalizeMXPF; 1744 | Pass; 1745 | except 1746 | on x: Exception do begin 1747 | if mxInitialized then FinalizeMXPF; 1748 | Fail(x); 1749 | end; 1750 | end; 1751 | 1752 | Describe('Multiple record signatures input'); 1753 | try 1754 | InitializeMXPF; 1755 | DefaultOptionsMXPF; 1756 | SetInclusions('Skyrim.esm'); 1757 | MultiLoad('ARMO,WEAP'); 1758 | Expect(mxLoadCalled, 'Should load records'); 1759 | ExpectEqual(mxRecords.Count, 5246, 'Should load the correct records'); 1760 | FinalizeMXPF; 1761 | Pass; 1762 | except 1763 | on x: Exception do begin 1764 | if mxInitialized then FinalizeMXPF; 1765 | Fail(x); 1766 | end; 1767 | end; 1768 | 1769 | Describe('Multiple signature pairs input'); 1770 | try 1771 | InitializeMXPF; 1772 | SetExclusions(mxBethesdaSkyrimFiles); 1773 | MultiLoad('CELL:ACHR,CELL:PGRE'); 1774 | Expect(mxLoadCalled, 'Should load records'); 1775 | ExpectEqual(mxRecords.Count, 26, 'Should load the correct records'); 1776 | FinalizeMXPF; 1777 | Pass; 1778 | except 1779 | on x: Exception do begin 1780 | if mxInitialized then FinalizeMXPF; 1781 | Fail(x); 1782 | end; 1783 | end; 1784 | 1785 | // All tests passed? 1786 | Pass; 1787 | except 1788 | on x: Exception do Fail(x); 1789 | end; 1790 | 1791 | // All tests passed? 1792 | Pass; 1793 | except 1794 | on x: Exception do Fail(x); 1795 | end; 1796 | end; 1797 | 1798 | 1799 | {******************************************************************************} 1800 | { ENTRY POINTS 1801 | Entry points for when the script is run in xEdit. 1802 | - Initialize 1803 | } 1804 | {******************************************************************************} 1805 | 1806 | function Initialize: Integer; 1807 | begin 1808 | // initialize jvt 1809 | jvtInitialize; 1810 | 1811 | // set up MXPF for testing environment 1812 | mxDisallowNewFile := true; 1813 | mxDisallowSaving := true; 1814 | mxDisallowPrinting := true; 1815 | 1816 | // perform tests 1817 | TestGeneral; 1818 | TestLogging; 1819 | TestPatchFileSelection; 1820 | TestFileSelection; 1821 | TestRecordProcessing; 1822 | TestRecordPatching; 1823 | TestMacros; 1824 | 1825 | // finalize jvt 1826 | jvtPrintReport; 1827 | jvtFinalize; 1828 | end; 1829 | 1830 | end. 1831 | -------------------------------------------------------------------------------- /Edit Scripts/jvTest - Example.pas: -------------------------------------------------------------------------------- 1 | unit UserScript; 2 | 3 | uses 'lib\jvTest'; 4 | 5 | procedure TestBooleans; 6 | begin 7 | Describe('True'); 8 | try 9 | Expect(true, 'Should pass'); 10 | Pass; 11 | except on x: Exception do 12 | Fail(x); 13 | end; 14 | 15 | Describe('False'); 16 | try 17 | Expect(false, 'Should not pass'); 18 | Pass; 19 | except on x: Exception do 20 | Fail(x); 21 | end; 22 | end; 23 | 24 | procedure TestIntegers; 25 | begin 26 | Describe('Equality'); 27 | try 28 | Expect(0 = 0, 'Zero should equal zero'); 29 | Expect(1 = 1, 'One should equal one'); 30 | Expect(2 = 2, 'Two should equal two'); 31 | Pass; 32 | except on x: Exception do 33 | Fail(x); 34 | end; 35 | 36 | Describe('Greater than'); 37 | try 38 | Expect(4 > 2, 'Four should be greater than two'); 39 | Expect(100 > -1, '100 should be greater than -1'); 40 | Expect(99999 > 0, '99999 should be greater than 0'); 41 | Pass; 42 | except on x: Exception do 43 | Fail(x); 44 | end; 45 | 46 | Describe('Less than'); 47 | try 48 | Expect(1 < 3, 'One should be less than three'); 49 | Expect(-3 < 1, '-3 should be less than 1'); 50 | Expect(9 < 10, '9 should be less than 10'); 51 | Pass; 52 | except on x: Exception do 53 | Fail(x); 54 | end; 55 | end; 56 | 57 | function Initialize: Integer; 58 | begin 59 | // initialize jvt 60 | jvtInitialize; 61 | 62 | // perform tests 63 | Describe('Booleans'); 64 | try 65 | TestBooleans; 66 | Pass; 67 | except on x: Exception do 68 | Fail(x); 69 | end; 70 | 71 | Describe('Integer Comparison'); 72 | try 73 | TestIntegers; 74 | Pass; 75 | except on x: Exception do 76 | Fail(x); 77 | end; 78 | 79 | Describe('ExpectEqual'); 80 | try 81 | ExpectEqual('Test', 'Test', 'Equal strings'); 82 | ExpectEqual(1, 1, 'Equal integers'); 83 | ExpectEqual(1.23, 1.23, 'Equal floats'); 84 | Pass; 85 | except on x: Exception do 86 | Fail(x); 87 | end; 88 | 89 | // finalize jvt 90 | jvtFinalize; 91 | end; 92 | 93 | end. -------------------------------------------------------------------------------- /Edit Scripts/lib/jvTest.pas: -------------------------------------------------------------------------------- 1 | { 2 | xEdit Testing Framework 3 | by matortheeternal 4 | 5 | This is a simple testing framework for executing tests 6 | in the jvInterpreter in xEdit. 7 | } 8 | 9 | unit jvTest; 10 | 11 | const 12 | { USER CONSTANTS } 13 | jvtPrintLog = true; 14 | jvtSaveLog = true; 15 | jvtEchoLog = false; 16 | 17 | { DEVELOPER CONSTANTS } 18 | jvtVersion = '0.1.0'; 19 | jvtTabSize = 4; 20 | jvtPassed = '[PASSED]'; 21 | jvtFailed = '[FAILED]'; 22 | 23 | var 24 | jvtMaxLevel, jvtTestsFailed, jvtTestsPassed, jvtSpecsFailed, jvtSpecsPassed: Integer; 25 | jvtInitialized: boolean; 26 | jvtLog, jvtFailures, jvtStack, jvtStackTrace: TStringList; 27 | 28 | 29 | //========================================================================= 30 | // LOG MESSAGES 31 | //========================================================================= 32 | procedure jvtLogMessage(msg: string); 33 | begin 34 | jvtLog.Add(msg); 35 | if jvtEchoLog then AddMessage(msg); 36 | end; 37 | 38 | procedure jvtLogTest(msg: string); 39 | var 40 | tab: string; 41 | begin 42 | tab := StringOfChar(' ', jvtTabSize * jvtStack.Count); 43 | jvtLogMessage(tab + msg); 44 | end; 45 | 46 | procedure jvtSaveLogMessages; 47 | var 48 | filename: string; 49 | begin 50 | // exit if no debug messages to save 51 | if (jvtLog.Count = 0) then exit; 52 | 53 | // save to mxpf logs folder in scripts path 54 | filename := ScriptsPath + 'logs\jvt\jvt-'+FormatDateTime('mmddyy_hhnnss', Now)+'.txt'; 55 | AddMessage('JVT Log saved to '+filename); 56 | ForceDirectories(ExtractFilePath(filename)); 57 | jvtLog.SaveToFile(filename); 58 | end; 59 | 60 | procedure jvtPrintLogMessages; 61 | begin 62 | if (jvtLog.Count = 0) then exit; 63 | AddMessage(jvtLog.Text); 64 | end; 65 | 66 | 67 | //========================================================================= 68 | // GENERAL 69 | //========================================================================= 70 | procedure jvtInitialize; 71 | begin 72 | jvtInitialized := true; 73 | jvtMaxLevel := 1; 74 | jvtTestsFailed := 0; 75 | jvtTestsPassed := 0; 76 | jvtSpecsPassed := 0; 77 | jvtSpecsFailed := 0; 78 | jvtLog := TStringList.Create; 79 | jvtStack := TStringList.Create; 80 | jvtFailures := TStringList.Create; 81 | jvtStackTrace := TStringList.Create; 82 | jvtLogMessage('JVT initialized at '+TimeToStr(Now)); 83 | jvtLogMessage(' '); 84 | end; 85 | 86 | procedure jvtPush(var sl: TStringList; s: string); 87 | begin 88 | sl.Add(s); 89 | end; 90 | 91 | procedure jvtPop(var sl: TStringList); 92 | begin 93 | sl.Delete(Pred(sl.Count)); 94 | end; 95 | 96 | function jvtGetStackTrace: string; 97 | var 98 | i: integer; 99 | begin 100 | for i := 0 to Pred(jvtStack.Count) do begin 101 | if Result <> '' then 102 | Result := Format('%s > %s', [Result, jvtStack[i]]) 103 | else 104 | Result := jvtStack[i]; 105 | end; 106 | end; 107 | 108 | function jvtHasTrace(t: string): boolean; 109 | var 110 | i: integer; 111 | begin 112 | for i := 0 to Pred(jvtStackTrace.Count) do begin 113 | if Pos(t, jvtStackTrace[i]) = 1 then begin 114 | Result := true; 115 | break; 116 | end; 117 | end; 118 | end; 119 | 120 | procedure jvtFinalize; 121 | begin 122 | // log finalization 123 | jvtLogMessage(' '); 124 | jvtLogMessage('JVT finalized at '+TimeToStr(Now)); 125 | 126 | // print/save messages 127 | if jvtPrintLog then jvtPrintLogMessages; 128 | if jvtSaveLog then jvtSaveLogMessages; 129 | 130 | // reset variables 131 | jvtInitialized := false; 132 | jvtTestsFailed := 0; 133 | jvtTestsPassed := 0; 134 | jvtMaxLevel := 0; 135 | 136 | // free memory used by lists 137 | jvtFailures.Free; 138 | jvtStack.Free; 139 | jvtStackTrace.Free; 140 | jvtLog.Free; 141 | end; 142 | 143 | 144 | //========================================================================= 145 | // TESTING 146 | //========================================================================= 147 | procedure Describe(name: string); 148 | begin 149 | jvtLogTest(name); 150 | jvtPush(jvtStack, name); 151 | end; 152 | 153 | procedure Pass; 154 | var 155 | index: Integer; 156 | begin 157 | index := jvtFailures.IndexOf(IntToStr(jvtStack.Count)); 158 | if index > -1 then begin 159 | jvtFailures.Delete(index); 160 | raise Exception.Create('')); 161 | end; 162 | 163 | if jvtMaxLevel >= jvtStack.Count then begin 164 | jvtLogTest(jvtPassed); 165 | jvtLogMessage(' '); 166 | end; 167 | Inc(jvtTestsPassed); 168 | jvtPop(jvtStack); 169 | end; 170 | 171 | procedure Fail(x: Exception); 172 | var 173 | trace, fail: string; 174 | begin 175 | fail := jvtFailed+' '+x.Message; 176 | jvtLogTest(fail); 177 | if jvtMaxLevel >= jvtStack.Count then 178 | jvtLogMessage(' '); 179 | 180 | Inc(jvtTestsFailed); 181 | trace := jvtGetStackTrace(); 182 | if not jvtHasTrace(trace) then 183 | jvtStackTrace.Add(Format('%s >> %s', [trace, fail])); 184 | jvtPop(jvtStack); 185 | jvtFailures.Add(IntToStr(jvtStack.Count)); 186 | end; 187 | 188 | procedure Expect(expectation: boolean; test: string); 189 | begin 190 | jvtLogTest(test); 191 | if not expectation then begin 192 | Inc(jvtSpecsFailed); 193 | raise Exception.Create(test); 194 | end; 195 | Inc(jvtSpecsPassed); 196 | end; 197 | 198 | procedure ExpectEqual(v1, v2: Variant; test: string); 199 | const 200 | varInteger = 3; 201 | varDouble = 5; 202 | varShortInt = 16; 203 | varString = 256; { Pascal string } 204 | varUString = 258; { Unicode string } 205 | { SEE http://stackoverflow.com/questions/24731098/ for more } 206 | var 207 | vt: Integer; 208 | begin 209 | jvtLogTest(test); 210 | if v1 <> v2 then begin 211 | Inc(jvtSpecsFailed); 212 | vt := VarType(v1); 213 | case vt of 214 | varInteger: raise Exception.Create(Format('Expected "%d", found "%d"', [v2, v1])); 215 | varShortInt: raise Exception.Create(Format('Expected "%d", found "%d"', [Integer(v2), Integer(v1)])); 216 | varDouble: raise Exception.Create(Format('Expected "%0.4f", found "%0.4f"', [v2, v1])); 217 | varString, varUString: raise Exception.Create(Format('Expected "%s", found "%s"', [v2, v1])); 218 | else raise Exception.Create(test + ', type ' + IntToStr(vt)); 219 | end; 220 | end; 221 | Inc(jvtSpecsPassed); 222 | end; 223 | 224 | 225 | //========================================================================= 226 | // REPORTING 227 | //========================================================================= 228 | procedure jvtPrintReport; 229 | var 230 | totalTests, totalSpecs, i: Integer; 231 | begin 232 | if not jvtInitialized then 233 | raise Exception.Create('JVT Error: You need to call jvtInitialize before calling jvtPrintReport'); 234 | 235 | jvtLogMessage(' '); 236 | jvtLogMessage('JVT Report:'); 237 | totalTests := jvtTestsPassed + jvtTestsFailed; 238 | totalSpecs := jvtSpecsPassed + jvtSpecsFailed; 239 | jvtLogMessage(Format('%d tests, %d passed, %d failed.', [totalTests, jvtTestsPassed, jvtTestsFailed])); 240 | jvtLogMessage(Format('%d specs, %d passed, %d failed.', [totalSpecs, jvtSpecsPassed, jvtSpecsFailed])); 241 | if jvtStackTrace.Count > 0 then begin 242 | jvtLogMessage(' '); 243 | jvtLogMessage('Stack trace:'); 244 | for i := 0 to Pred(jvtStackTrace.Count) do 245 | jvtLogMessage(jvtStackTrace[i]); 246 | end; 247 | end; 248 | 249 | end. -------------------------------------------------------------------------------- /Edit Scripts/lib/mxpf.pas: -------------------------------------------------------------------------------- 1 | { 2 | Mator's xEdit Patching Framework 3 | by matortheeternal 4 | } 5 | 6 | unit mxpf; 7 | 8 | uses 'lib\mteFunctions'; 9 | 10 | const 11 | { USER CONSTANTS - FEEL FREE TO CHANGE } 12 | // debug constants 13 | mxDebug = true; 14 | mxDebugVerbose = false; 15 | 16 | // logging constants 17 | mxSaveDebug = true; 18 | mxSaveFailures = true; 19 | mxPrintDebug = false; 20 | mxPrintFailures = true; 21 | mxEchoDebug = false; 22 | mxEchoFailures = false; 23 | 24 | { DEVELOPER CONSTANTS - DON'T CHANGE } 25 | // version constant 26 | mxVersion = '0.2.0'; 27 | 28 | // mode constants 29 | mxExclusionMode = 1; 30 | mxInclusionMode = 2; 31 | 32 | // comma separated list of bethesda skyrim files 33 | mxBethesdaSkyrimFiles = 'Skyrim.esm'#44'Update.esm'#44'Dawnguard.esm'#44'HearthFires.esm'#44 34 | 'Dragonborn.esm'#44 35 | 'Skyrim.Hardcoded.keep.this.with.the.exe.and.otherwise.ignore.it.I.really.mean.it.dat'#44 36 | 'Skyrim.exe'; 37 | 38 | // comma separated list of hardcoded dat files 39 | mxHardcodedDatFiles = 40 | 'Skyrim.Hardcoded.keep.this.with.the.exe.and.otherwise.ignore.it.I.really.mean.it.dat'#44 41 | 'Fallout3.Hardcoded.keep.this.with.the.exe.and.otherwise.ignore.it.I.really.mean.it.dat'#44 42 | 'Oblivion.Hardcoded.keep.this.with.the.exe.and.otherwise.ignore.it.I.really.mean.it.dat'#44 43 | 'FalloutNV.Hardcoded.keep.this.with.the.exe.and.otherwise.ignore.it.I.really.mean.it.dat'#44 44 | 'Fallout4.Hardcoded.keep.this.with.the.exe.and.otherwise.ignore.it.I.really.mean.it.dat'#44 45 | 'Skyrim.exe'#44'Fallout3.exe'#44'Oblivion.exe'#44'FalloutNV.exe'#44'Fallout4.exe'; 46 | 47 | var 48 | mxFiles, mxMasters, mxDebugMessages, mxFailureMessages: TStringList; 49 | mxRecords, mxPatchRecords: TList; 50 | mxFileMode, mxRecordsCopied, mxRecordsFound, mxMasterRecords, 51 | mxOverrideRecords: Integer; 52 | mxInitialized, mxLoadCalled, mxCopyCalled, mxLoadMasterRecords, 53 | mxLoadOverrideRecords, mxLoadWinningOverrides, mxMastersAdded, 54 | mxSkipPatchedRecords, mxDisallowNewFile, mxDisallowSaving, 55 | mxDisallowPrinting: boolean; 56 | mxPatchFile: IInterface; 57 | 58 | //========================================================================= 59 | // DEBUG MESSAGES 60 | //========================================================================= 61 | procedure DebugMessage(s: string); 62 | begin 63 | mxDebugMessages.Add(s); 64 | if mxEchoDebug then AddMessage(s); 65 | end; 66 | 67 | procedure DebugList(var sl: TStringList; pre: string); 68 | var 69 | i: integer; 70 | begin 71 | for i := 0 to Pred(sl.Count) do 72 | DebugMessage(pre + sl[i]); 73 | end; 74 | 75 | procedure SaveDebugMessages; 76 | var 77 | filename: string; 78 | begin 79 | // exit if no debug messages to save 80 | if (mxDebugMessages.Count = 0) then exit; 81 | 82 | // save to mxpf logs folder in scripts path 83 | filename := ScriptsPath + 'logs\mxpf\mxpf-debug-'+FileDateTimeStr(Now)+'.txt'; 84 | AddMessage('MXPF Debug Log saved to '+filename); 85 | ForceDirectories(ExtractFilePath(filename)); 86 | mxDebugMessages.SaveToFile(filename); 87 | end; 88 | 89 | procedure PrintDebugMessages; 90 | begin 91 | // exit if no debug messages to print 92 | if (mxDebugMessages.Count = 0) then exit; 93 | 94 | // else print to xEdit's log 95 | AddMessage(mxDebugMessages.Text); 96 | end; 97 | 98 | //========================================================================= 99 | // FAILURE MESSAGES 100 | //========================================================================= 101 | procedure FailureMessage(s: string); 102 | begin 103 | mxFailureMessages.Add(s); 104 | if mxEchoFailures then AddMessage(s); 105 | end; 106 | 107 | procedure SaveFailureMessages; 108 | var 109 | filename: string; 110 | begin 111 | // exit if no failure messages to save 112 | if (mxFailureMessages.Count = 0) then exit; 113 | 114 | // save to mxpf logs folder in scripts path 115 | filename := ScriptsPath + 'logs\mxpf\mxpf-failures-'+FileDateTimeStr(Now)+'.txt'; 116 | AddMessage('MXPF Failures Log saved to '+filename); 117 | ForceDirectories(ExtractFilePath(filename)); 118 | mxFailureMessages.SaveToFile(filename); 119 | end; 120 | 121 | procedure PrintFailureMessages; 122 | begin 123 | // exit if no failure messages to print 124 | if (mxFailureMessages.Count = 0) then exit; 125 | 126 | // else print to xEdit's log 127 | AddMessage(mxFailureMessages.Text); 128 | end; 129 | 130 | //========================================================================= 131 | // GENERAL 132 | //========================================================================= 133 | procedure InitializeMXPF; 134 | begin 135 | mxInitialized := true; 136 | mxDebugMessages := TStringList.Create; 137 | mxFailureMessages := TStringList.Create; 138 | mxMasters := TStringList.Create; 139 | mxMasters.Sorted := true; 140 | mxMasters.Duplicates := dupIgnore; 141 | mxFiles := TStringList.Create; 142 | mxRecords := TList.Create; 143 | mxPatchRecords := TList.Create; 144 | if mxDebug then begin 145 | DebugMessage('MXPF Initialized at '+TimeStr(Now)); 146 | DebugMessage(' '); 147 | end; 148 | end; 149 | 150 | procedure DefaultOptionsMXPF; 151 | begin 152 | mxLoadMasterRecords := true; 153 | mxSkipPatchedRecords := true; 154 | mxLoadWinningOverrides := true; 155 | end; 156 | 157 | procedure FinalizeMXPF; 158 | begin 159 | // clean masters on mxPatchFile if it exists 160 | if Assigned(mxPatchFile) then 161 | CleanMasters(mxPatchFile); 162 | 163 | // log finalization 164 | if mxDebug then begin 165 | DebugMessage(' '); 166 | DebugMessage('MXPF Finalized at '+TimeStr(Now)); 167 | end; 168 | 169 | // print messages 170 | if not mxDisallowPrinting then begin 171 | if mxPrintDebug then PrintDebugMessages; 172 | if mxPrintFailures then PrintFailureMessages; 173 | end; 174 | // save messages 175 | if not mxDisallowSaving then begin 176 | if mxSaveDebug then SaveDebugMessages; 177 | if mxSaveFailures then SaveFailureMessages; 178 | end; 179 | 180 | // reset variables 181 | mxInitialized := false; 182 | mxLoadCalled := false; 183 | mxCopyCalled := false; 184 | mxLoadMasterRecords := false; 185 | mxLoadOverrideRecords := false; 186 | mxLoadWinningOverrides := false; 187 | mxFileMode := 0; 188 | mxRecordsCopied := 0; 189 | mxPatchFile := nil; 190 | 191 | // free memory allocated for lists 192 | FreeAndNil(mxDebugMessages); 193 | FreeAndNil(mxFailureMessages); 194 | FreeAndNil(mxFiles); 195 | FreeAndNil(mxMasters); 196 | FreeAndNil(mxRecords); 197 | FreeAndNil(mxPatchRecords); 198 | end; 199 | 200 | 201 | //========================================================================= 202 | // PATCH FILE SELECTION 203 | //========================================================================= 204 | procedure PatchFileByAuthor(author: string); 205 | var 206 | madeNewFile: boolean; 207 | begin 208 | // if user hasn't initialized MXPF, raise exception 209 | if not mxInitialized then 210 | raise Exception.Create('MXPF Error: You need to call InitializeMXPF before calling PatchFileByAuthor'); 211 | 212 | // select existing file or create new one 213 | madeNewFile := false; 214 | mxPatchFile := FileByAuthor(author); 215 | if not (Assigned(mxPatchFile) or mxDisallowNewFile) then begin 216 | mxPatchFile := AddNewFile; 217 | SetAuthor(mxPatchFile, author); 218 | madeNewFile := true; 219 | end; 220 | 221 | // print debug messages 222 | if mxDebug then begin 223 | if madeNewFile then 224 | DebugMessage(Format('MXPF: Made new file %s, with author %s', [GetFileName(mxPatchFile), GetAuthor(mxPatchFile)])) 225 | else 226 | DebugMessage(Format('MXPF: Using patch file %s', [GetFileName(mxPatchFile)])); 227 | DebugMessage(' '); 228 | end; 229 | end; 230 | 231 | procedure PatchFileByName(filename: string); 232 | var 233 | madeNewFile: boolean; 234 | begin 235 | // if user hasn't initialized MXPF, raise exception 236 | if not mxInitialized then 237 | raise Exception.Create('MXPF Error: You need to call InitializeMXPF before calling PatchFileByName'); 238 | 239 | // select existing file or create new one 240 | madeNewFile := false; 241 | mxPatchFile := FileByName(filename); 242 | if not (Assigned(mxPatchFile) or mxDisallowNewFile) then begin 243 | ShowMessage('Enter "'+ChangeFileExt(filename, '')+'" for the patch filename in the next window.'); 244 | mxPatchFile := AddNewFileName(filename); 245 | madeNewFile := true; 246 | end; 247 | 248 | // if user entered invalid filename, tell them 249 | if not SameText(GetFileName(mxPatchFile), filename) then 250 | ShowMessage('You entered an incorrect filename. The script will not recognize this file as the patch in the future.'); 251 | 252 | // print debug messages 253 | if mxDebug then begin 254 | if madeNewFile then 255 | DebugMessage(Format('MXPF: Made new file %s', [GetFileName(mxPatchFile)])) 256 | else 257 | DebugMessage(Format('MXPF: Using patch file %s', [GetFileName(mxPatchFile)])); 258 | DebugMessage(' '); 259 | end; 260 | end; 261 | 262 | //========================================================================= 263 | // FILE EXCLUSIONS / INCLUSIONS 264 | //========================================================================= 265 | procedure SetExclusions(s: string); 266 | begin 267 | // if user hasn't initialized MXPF, raise exception 268 | if not mxInitialized then 269 | raise Exception.Create('MXPF Error: You need to call InitializeMXPF before calling SetExclusions'); 270 | 271 | // set files to string 272 | mxFileMode := mxExclusionMode; 273 | mxFiles.CommaText := s; 274 | 275 | // print debug messages if in debug mode 276 | if mxDebug then begin 277 | DebugMessage('MXPF: Set exclusions to:'); 278 | DebugList(mxFiles, ' '); 279 | DebugMessage(' '); 280 | end; 281 | end; 282 | 283 | procedure SetInclusions(s: string); 284 | begin 285 | // if user hasn't initialized MXPF, raise exception 286 | if not mxInitialized then 287 | raise Exception.Create('MXPF Error: You need to call InitializeMXPF before calling SetInclusions'); 288 | 289 | // set files to string 290 | mxFileMode := mxInclusionMode; 291 | mxFiles.CommaText := s; 292 | 293 | // print debug messages if in debug mode 294 | if mxDebug then begin 295 | DebugMessage('MXPF: Set inclusions to:'); 296 | DebugList(mxFiles, ' '); 297 | DebugMessage(' '); 298 | end; 299 | end; 300 | 301 | //========================================================================= 302 | // RECORD PROCESSING 303 | //========================================================================= 304 | procedure LoadRecords(sig: string); 305 | var 306 | start: TDateTime; 307 | i, j, n: Integer; 308 | f, g, rec: IInterface; 309 | filename: string; 310 | begin 311 | // if user hasn't initialized MXPF, raise exception 312 | if not mxInitialized then 313 | raise Exception.Create('MXPF Error: You need to call InitializeMXPF before calling LoadRecords'); 314 | 315 | // set boolean so we know the user called this function 316 | mxLoadCalled := true; 317 | // track time so we know how long the load takes 318 | start := Now; 319 | // set mxMastersAdded to false because they may change 320 | mxMastersAdded := false; 321 | 322 | // loop through files 323 | DebugMessage('MXPF: Loading records matching signature '+sig); 324 | for i := 0 to Pred(FileCount) do begin 325 | f := FileByIndex(i); 326 | filename := GetFileName(f); 327 | 328 | // skip patch file 329 | if Assigned(mxPatchFile) then begin 330 | if filename = GetFileName(mxPatchFile) then begin 331 | if mxDebug then DebugMessage(' Skipping patch file '+filename); 332 | break; 333 | end; 334 | end; 335 | 336 | // handle file mode 337 | if mxFileMode = mxExclusionMode then begin 338 | // skip files if in exclusion mode 339 | if mxFiles.IndexOf(filename) > -1 then begin 340 | if mxDebug then DebugMessage(' Skipping excluded file '+filename); 341 | continue; 342 | end; 343 | end 344 | else if mxFileMode = mxInclusionMode then begin 345 | // include files if in inclusion mode 346 | if mxFiles.IndexOf(filename) = -1 then begin 347 | if mxDebug then DebugMessage(' Skipping file '+filename); 348 | continue; 349 | end; 350 | end; 351 | 352 | // get group 353 | DebugMessage(' Processing file '+filename); 354 | g := GroupBySignature(f, sig); 355 | 356 | // skip if group not found 357 | if not Assigned(g) then begin 358 | if mxDebug then DebugMessage(' Group '+sig+' not found.'); 359 | continue; 360 | end; 361 | 362 | // add masters 363 | AddMastersToList(f, mxMasters); 364 | 365 | mxRecordsFound := 0; 366 | mxMasterRecords := 0; 367 | mxOverrideRecords := 0; 368 | // loop through records in group 369 | for j := 0 to Pred(ElementCount(g)) do begin 370 | rec := ElementByIndex(g, j); 371 | 372 | // if restricted to master records only, skip if not master record 373 | if mxLoadMasterRecords and not IsMaster(rec) then begin 374 | if mxDebug and mxDebugVerbose then DebugMessage(' Skipping override record '+Name(rec)); 375 | Inc(mxOverrideRecords); 376 | continue; 377 | end; 378 | 379 | // if restricted to override records only, skip if not override record 380 | if mxLoadOverrideRecords and IsMaster(rec) then begin 381 | if mxDebug and mxDebugVerbose then DebugMessage(' Skipping master record '+Name(rec)); 382 | Inc(mxMasterRecords); 383 | continue; 384 | end; 385 | 386 | // if loading winning override records, get winning override 387 | if mxLoadWinningOverrides then begin 388 | try 389 | rec := WinningOverrideBefore(rec, mxPatchFile); 390 | if mxDebug and mxDebugVerbose then DebugMessage(Format(' Using override from %s', 391 | [GetFileName(GetFile(rec))])); 392 | except 393 | on x: Exception do begin 394 | DebugMessage(' Exception getting winning override for '+Name(rec)+', '+x.Message); 395 | continue; 396 | end; 397 | end; 398 | end; 399 | 400 | // add record to list 401 | if mxDebug and mxDebugVerbose then DebugMessage(' Found record '+Name(rec)); 402 | mxRecords.Add(TObject(rec)); 403 | Inc(mxRecordsFound); 404 | end; 405 | 406 | // print number of records we added to the list 407 | if mxDebug and not mxDebugVerbose then begin 408 | DebugMessage(Format(' Found %d records', [mxRecordsFound])); 409 | if mxLoadMasterRecords then 410 | DebugMessage(Format(' Skipped %d override records', [mxOverrideRecords])); 411 | if mxLoadOverrideRecords then 412 | DebugMessage(Format(' Skipped %d master records', [mxMasterRecords])); 413 | end; 414 | end; 415 | 416 | // print final debug messages 417 | if mxDebug then begin 418 | if mxRecords.Count > 0 then 419 | DebugMessage(Format('MXPF: Loaded %d records in %0.2fs', [mxRecords.Count, Now - start])) 420 | else 421 | DebugMessage('MXPF: Couldn''t find any records matching signature '+sig); 422 | DebugMessage(' '); 423 | end; 424 | end; 425 | 426 | procedure LoadChildRecords(groupSig, sig: string); 427 | var 428 | start: TDateTime; 429 | i, j: Integer; 430 | f, g, rec: IInterface; 431 | filename: string; 432 | begin 433 | // if user hasn't initialized MXPF, raise exception 434 | if not mxInitialized then 435 | raise Exception.Create('MXPF Error: You need to call InitializeMXPF before calling LoadChildRecords'); 436 | 437 | // set boolean so we know the user called this function 438 | mxLoadCalled := true; 439 | // track time so we know how long the load takes 440 | start := Now; 441 | // set mxMastersAdded to false because they may change 442 | mxMastersAdded := false; 443 | // set mxRecordsFound to 0 so it is not nil 444 | mxRecordsFound := 0; 445 | 446 | // loop through files 447 | DebugMessage('MXPF: Loading records matching signature '+sig); 448 | for i := 0 to Pred(FileCount) do begin 449 | f := FileByIndex(i); 450 | filename := GetFileName(f); 451 | 452 | // skip patch file 453 | if filename = GetFileName(mxPatchFile) then begin 454 | if mxDebug then DebugMessage(' Skipping patch file '+filename); 455 | break; 456 | end; 457 | 458 | // handle file mode 459 | if mxFileMode = mxExclusionMode then begin 460 | // skip files if in exclusion mode 461 | if mxFiles.IndexOf(filename) > -1 then begin 462 | if mxDebug then DebugMessage(' Skipping excluded file '+filename); 463 | continue; 464 | end; 465 | end 466 | else if mxFileMode = mxInclusionMode then begin 467 | // include files if in inclusion mode 468 | if mxFiles.IndexOf(filename) = -1 then begin 469 | if mxDebug then DebugMessage(' Skipping file '+filename); 470 | continue; 471 | end; 472 | end; 473 | 474 | // get group 475 | DebugMessage(' Processing file '+filename); 476 | g := GroupBySignature(f, groupSig); 477 | 478 | // skip if group not found 479 | if not Assigned(g) then begin 480 | if mxDebug then DebugMessage(' Group '+groupSig+' not found.'); 481 | continue; 482 | end; 483 | 484 | // add masters 485 | AddMastersToList(f, mxMasters); 486 | 487 | // load records with wbGetSiblingRecords 488 | wbGetSiblingRecords(g, sig, false, mxRecords); 489 | mxOverrideRecords := 0; 490 | mxMasterRecords := 0; 491 | mxRecordsFound := mxRecords.Count - mxRecordsFound; 492 | 493 | // filter records 494 | if (mxLoadMasterRecords or mxLoadOverrideRecords or mxLoadWinningOverrides) then begin 495 | for j := Pred(mxRecords.Count) downto 0 do begin 496 | rec := ObjectToElement(mxRecords[j]); 497 | 498 | // if restricted to master records only, remove if not master record 499 | if mxLoadMasterRecords and not IsMaster(rec) then begin 500 | if mxDebug and mxDebugVerbose then DebugMessage(' Removing override record '+Name(rec)); 501 | mxRecords.Delete(j); 502 | Inc(mxOverrideRecords); 503 | continue; 504 | end; 505 | 506 | // if restricted to override records only, skip if not override record 507 | if mxLoadOverrideRecords and IsMaster(rec) then begin 508 | if mxDebug and mxDebugVerbose then DebugMessage(' Removing master record '+Name(rec)); 509 | mxRecords.Delete(j); 510 | Inc(mxMasterRecords); 511 | continue; 512 | end; 513 | 514 | // if loading winning override records, get winning override 515 | if mxLoadWinningOverrides then try 516 | rec := WinningOverrideBefore(rec, mxPatchFile); 517 | if mxDebug and mxDebugVerbose then DebugMessage(' Loading winning override from '+GetFileName(GetFile(rec))); 518 | mxRecords[j] := TObject(rec); 519 | except 520 | on x: Exception do begin 521 | DebugMessage(' Exception getting winning override for '+Name(rec)+', '+x.Message); 522 | mxRecords.Delete(j); 523 | continue; 524 | end; 525 | end; 526 | end; 527 | end; 528 | 529 | // print number of records we added to the list 530 | if mxDebug and not mxDebugVerbose then begin 531 | DebugMessage(Format(' Found %d records', [mxRecordsFound])); 532 | if mxLoadMasterRecords then 533 | DebugMessage(Format(' Removed %d override records', [mxOverrideRecords])); 534 | if mxLoadWinningOverrides then 535 | DebugMessage(Format(' Removed %d master records', [mxMasterRecords])); 536 | end; 537 | end; 538 | 539 | // print final debug messages 540 | if mxDebug then begin 541 | if mxRecords.Count > 0 then 542 | DebugMessage(Format('MXPF: Loaded %d records in %0.2fs', [mxRecords.Count, Now - start])) 543 | else 544 | DebugMessage('MXPF: Couldn''t find any records matching signature '+sig); 545 | DebugMessage(' '); 546 | end; 547 | end; 548 | 549 | function MaxRecordIndex: Integer; 550 | begin 551 | // if user hasn't initialized MXPF, raise exception 552 | if not mxInitialized then 553 | raise Exception.Create('MXPF Error: You need to call InitialzeMXPF before calling MaxRecordIndex'); 554 | 555 | // return value if checks pass 556 | Result := mxRecords.Count - 1; 557 | if mxDebugVerbose then DebugMessage(Format('MXPF: MaxRecordIndex returned %d', [Result])); 558 | end; 559 | 560 | function GetRecord(i: integer): IInterface; 561 | begin 562 | // if user hasn't initialized MXPF, raise exception 563 | if not mxInitialized then 564 | raise Exception.Create('MXPF Error: You need to call InitialzeMXPF before calling LoadRecords'); 565 | // if user hasn't loaded records, raise exception 566 | if not mxLoadCalled then 567 | raise Exception.Create('MXPF Error: You need to call LoadRecords before you can access records using GetRecord'); 568 | // if no records available, raise exception 569 | if mxRecords.Count = 0 then 570 | raise Exception.Create('MXPF Error: Can''t call GetRecord, no records available'); 571 | // if index is out of bounds, raise an exception 572 | if (i < 0) or (i > MaxRecordIndex) then 573 | raise Exception.Create('MXPF Error: GetRecord index out of bounds'); 574 | 575 | // if all checks pass, return record at user specified index 576 | Result := ObjectToElement(mxRecords[i]); 577 | if mxDebug then DebugMessage(Format('MXPF: GetRecord at index %d returned %s', [i, Name(Result)])); 578 | end; 579 | 580 | procedure RemoveRecord(i: integer); 581 | var 582 | n: string; 583 | begin 584 | // if user hasn't initialized MXPF, raise exception 585 | if not mxInitialized then 586 | raise Exception.Create('MXPF Error: You need to call InitialzeMXPF before calling RemoveRecord'); 587 | // if user hasn't loaded records, raise exception 588 | if not mxLoadCalled then 589 | raise Exception.Create('MXPF Error: You need to call LoadRecords before you can remove records using RemoveRecord'); 590 | // if no records available, raise exception 591 | if mxRecords.Count = 0 then 592 | raise Exception.Create('MXPF Error: Can''t call RemoveRecord, no records available'); 593 | // if index is out of bounds, raise an exception 594 | if (i < 0) or (i > MaxRecordIndex) then 595 | raise Exception.Create('MXPF Error: RemoveRecord index out of bounds'); 596 | 597 | // if all checks pass, remove record at user specified index 598 | n := Name(ObjectToElement(mxRecords[i])); 599 | mxRecords.Delete(i); 600 | if mxDebug then DebugMessage(Format('MXPF: Removed record at index %d, %s', [i, n])); 601 | end; 602 | 603 | //========================================================================= 604 | // RECORD PATCHING 605 | //========================================================================= 606 | procedure AddMastersToPatch; 607 | begin 608 | // if user hasn't initialized MXPF, raise exception 609 | if not mxInitialized then 610 | raise Exception.Create('MXPF Error: You need to call InitialzeMXPF before calling AddMastersToPatch'); 611 | // if user hasn't loaded records, raise exception 612 | if not mxLoadCalled then 613 | raise Exception.Create('MXPF Error: You need to call LoadRecords before you can call AddMastersToPatch'); 614 | // if user hasn't assigned a patch file, raise exception 615 | if not Assigned(mxPatchFile) then 616 | raise Exception.Create('MXPF Error: You need to assign mxPatchFile using PatchFileByAuthor or PatchFileByName before calling AddMastersToPatch'); 617 | 618 | // add masters to mxPatchFile 619 | AddMastersToFile(mxPatchFile, mxMasters, true); 620 | mxMastersAdded := true; 621 | if mxDebug then begin 622 | DebugMessage('MXPF: Added masters to patch file.'); 623 | DebugList(mxMasters, ' '); 624 | DebugMessage(' '); 625 | end; 626 | end; 627 | 628 | function CopyRecordToPatch(i: integer): IInterface; 629 | var 630 | rec: IInterface; 631 | begin 632 | // if user hasn't initialized MXPF, raise exception 633 | if not mxInitialized then 634 | raise Exception.Create('MXPF Error: You need to call InitialzeMXPF before calling CopyRecordToPatch'); 635 | // if user hasn't loaded records, raise exception 636 | if not mxLoadCalled then 637 | raise Exception.Create('MXPF Error: You need to call LoadRecords before you can copy records using CopyRecordToPatch'); 638 | // if user hasn't assigned a patch file, raise exception 639 | if not Assigned(mxPatchFile) then 640 | raise Exception.Create('MXPF Error: You need to assign mxPatchFile using PatchFileByAuthor or PatchFileByName before calling CopyRecordToPatch'); 641 | // if no records available, raise exception 642 | if mxRecords.Count = 0 then 643 | raise Exception.Create('MXPF Error: Can''t call CopyRecordToPatch, no records available'); 644 | // if index is out of bounds, raise an exception 645 | if (i < 0) or (i > MaxRecordIndex) then 646 | raise Exception.Create('MXPF Error: CopyRecordToPatch index out of bounds'); 647 | 648 | // if all checks pass, try copying record 649 | rec := ObjectToElement(mxRecords[i]); 650 | 651 | // exit if record already exists in patch 652 | if mxSkipPatchedRecords and OverrideExistsIn(rec, mxPatchFile) then begin 653 | DebugMessage(Format('Skipping record %s, already in patch!', [Name(rec)])); 654 | exit; 655 | end; 656 | 657 | // set boolean so we know the user called this function 658 | mxCopyCalled := true; 659 | 660 | // add masters to patch file if we haven't already 661 | if not mxMastersAdded then AddMastersToPatch; 662 | 663 | // copy record to patch 664 | try 665 | Result := wbCopyElementToFile(rec, mxPatchFile, false, true); 666 | mxPatchRecords.Add(TObject(Result)); 667 | if mxDebug then DebugMessage(Format('Copied record %s to patch file', [Name(Result)])); 668 | except on x: Exception do 669 | FailureMessage(Format('Failed to copy record %s, Exception: %s', [Name(rec), x.Message])); 670 | end; 671 | end; 672 | 673 | procedure CopyRecordsToPatch; 674 | var 675 | i: integer; 676 | start: TDateTime; 677 | rec, patchRec: IInterface; 678 | begin 679 | // if user hasn't initialized MXPF, raise exception 680 | if not mxInitialized then 681 | raise Exception.Create('MXPF Error: You need to call InitialzeMXPF before calling CopyRecordsToPatch'); 682 | // if user hasn't loaded records, raise exception 683 | if not mxLoadCalled then 684 | raise Exception.Create('MXPF Error: You need to call LoadRecords before you can copy records using CopyRecordsToPatch'); 685 | // if user hasn't assigned a patch file, raise exception 686 | if not Assigned(mxPatchFile) then 687 | raise Exception.Create('MXPF Error: You need to assign mxPatchFile using PatchFileByAuthor or PatchFileByName before calling CopyRecordsToPatch'); 688 | // if no records available, raise exception 689 | if mxRecords.Count = 0 then 690 | raise Exception.Create('MXPF Error: Can''t call CopyRecordsToPatch, no records available'); 691 | 692 | // set boolean so we know the user called this function 693 | mxCopyCalled := true; 694 | // track time so we know how long the load takes 695 | start := Now; 696 | 697 | // add masters to patch file if we haven't already 698 | if not mxMastersAdded then AddMastersToPatch; 699 | 700 | // log message 701 | DebugMessage('MXPF: Copying records to patch '+GetFileName(mxPatchfile)); 702 | 703 | // if all checks pass, loop through records list 704 | for i := 0 to Pred(mxRecords.Count) do begin 705 | rec := ObjectToElement(mxRecords[i]); 706 | 707 | // winningOverrideBefore failed 708 | if not Assigned(rec) then begin 709 | FailureMessage(Format('Failed to copy record %s, WinningOverrideBefore failure', [Name(rec)])); 710 | continue; 711 | end; 712 | 713 | // record already in patch 714 | if mxSkipPatchedRecords and OverrideExistsIn(rec, mxPatchFile) then begin 715 | DebugMessage(Format(' Skipping record %s, already in patch!', [Name(rec)])); 716 | continue; 717 | end; 718 | 719 | // try copying the record 720 | try 721 | patchRec := wbCopyElementToFile(rec, mxPatchFile, false, true); 722 | if not Assigned(patchRec) then 723 | raise Exception.Create('patchRec not assigned'); 724 | mxPatchRecords.Add(TObject(patchRec)); 725 | if mxDebug then DebugMessage(Format(' Copied record %s to patch file', [Name(patchRec)])); 726 | except on x: Exception do 727 | FailureMessage(Format('Failed to copy record %s, Exception: %s', [Name(rec), x.Message])); 728 | end; 729 | end; 730 | 731 | // print final debug messages 732 | if mxDebug then begin 733 | if mxPatchRecords.Count > 0 then 734 | DebugMessage(Format('MXPF: Copied %d records in %0.2fs', [mxPatchRecords.Count, Now - start])) 735 | else 736 | DebugMessage('MXPF: No records copied.'); 737 | DebugMessage(' '); 738 | end; 739 | end; 740 | 741 | function MaxPatchRecordIndex: Integer; 742 | begin 743 | // if user hasn't initialized MXPF, raise exception 744 | if not mxInitialized then 745 | raise Exception.Create('MXPF Error: You need to call InitialzeMXPF before calling MaxPatchRecordIndex'); 746 | 747 | // return value if checks pass 748 | Result := mxPatchRecords.Count - 1; 749 | if mxDebug then DebugMessage(Format('MXPF: MaxPatchRecordIndex returned %d', [Result])); 750 | end; 751 | 752 | function GetPatchRecord(i: Integer): IInterface; 753 | begin 754 | // if user hasn't initialized MXPF, raise exception 755 | if not mxInitialized then 756 | raise Exception.Create('MXPF Error: You need to call InitialzeMXPF before calling GetPatchRecord'); 757 | // if user hasn't loaded records, raise exception 758 | if not mxCopyCalled then 759 | raise Exception.Create('MXPF Error: You need to call CopyRecordsToPatch or CopyRecordToPatch before you can access records using GetPatchRecord'); 760 | // if index is out of bounds, raise an exception 761 | if (i < 0) or (i > MaxPatchRecordIndex) then 762 | raise Exception.Create('MXPF Error: GetPatchRecord index out of bounds'); 763 | 764 | // if all checks pass, return record at user specified index 765 | Result := ObjectToElement(mxPatchRecords[i]); 766 | if mxDebug then DebugMessage(Format('MXPF: GetPatchRecord at index %d returned %s', [i, Name(Result)])); 767 | end; 768 | 769 | //========================================================================= 770 | // MACROS 771 | //========================================================================= 772 | 773 | function MultiFileSelectString(sPrompt: String; var sFiles: String): Boolean; 774 | var 775 | sl: TStringList; 776 | begin 777 | sl := TStringList.Create; 778 | try 779 | Result := MultiFileSelect(sl, sPrompt); 780 | sFiles := sl.CommaText; 781 | finally 782 | sl.Free; 783 | end; 784 | end; 785 | 786 | procedure SetFileSelection(sFiles: String; bMode: Boolean); 787 | begin 788 | // inclusions mode if bMode is true 789 | // exclusions mode if bMode is false 790 | if bMode then 791 | SetInclusions(sFiles) 792 | else 793 | SetExclusions(sFiles); 794 | end; 795 | 796 | procedure MultiLoad(sRecords: String); 797 | var 798 | sl: TStringList; 799 | i, index: Integer; 800 | sig, groupSig: String; 801 | begin 802 | sl := TStringList.Create; 803 | try 804 | // split on commas 805 | sl.CommaText := sRecords; 806 | for i := 0 to Pred(sl.Count) do begin 807 | // each token should be a record signature 808 | sig := sl[i]; 809 | 810 | // LoadChildRecords when there are two signatures separated 811 | // by a colon. E.g. WRLD:REFR 812 | index := Pos(':', sig); 813 | if index > 0 then begin 814 | groupSig := Copy(sig, 1, 4); 815 | sig := Copy(sig, 6, 9); 816 | LoadChildRecords(groupSig, sig); 817 | end 818 | // else just LoadRecords 819 | else 820 | LoadRecords(sig); 821 | end; 822 | finally 823 | sl.Free; 824 | end; 825 | end; 826 | 827 | procedure QuickLoad(sFiles, sRecords: String; bMode: Boolean); 828 | begin 829 | InitializeMXPF; 830 | DefaultOptionsMXPF; 831 | SetFileSelection(sFiles, bMode); 832 | MultiLoad(sRecords); 833 | end; 834 | 835 | procedure QuickPatch(sAuthor, sFiles, sRecords: String; bMode: Boolean); 836 | begin 837 | InitializeMXPF; 838 | DefaultOptionsMXPF; 839 | PatchFileByAuthor(sAuthor); 840 | SetFileSelection(sFiles, bMode); 841 | MultiLoad(sRecords); 842 | CopyRecordsToPatch; 843 | end; 844 | 845 | //========================================================================= 846 | // REPORTING 847 | //========================================================================= 848 | procedure PrintMXPFReport; 849 | var 850 | success, failure, total: Integer; 851 | begin 852 | // if user hasn't initialized MXPF, raise exception 853 | if not mxInitialized then 854 | raise Exception.Create('MXPF Error: You need to call InitialzeMXPF before calling PrintMXPFReport'); 855 | 856 | // print report 857 | AddMessage(' '); 858 | AddMessage('MXPF Record Copying Report:'); 859 | success := mxPatchRecords.Count; 860 | failure := mxFailureMessages.Count; 861 | total := success + failure; 862 | AddMessage(Format('%d copy operations, %d successful, %d failed.', [total, success, failure])); 863 | AddMessage(' '); 864 | end; 865 | 866 | end. 867 | -------------------------------------------------------------------------------- /Images/BuiltWithMXPF.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matortheeternal/mxpf/26e834dfe3b00594b7d986ecd48b5f513226907a/Images/BuiltWithMXPF.png -------------------------------------------------------------------------------- /Images/BuiltWithMXPF.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matortheeternal/mxpf/26e834dfe3b00594b7d986ecd48b5f513226907a/Images/BuiltWithMXPF.xcf -------------------------------------------------------------------------------- /Images/GettingStarted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matortheeternal/mxpf/26e834dfe3b00594b7d986ecd48b5f513226907a/Images/GettingStarted.png -------------------------------------------------------------------------------- /Images/GettingStarted.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matortheeternal/mxpf/26e834dfe3b00594b7d986ecd48b5f513226907a/Images/GettingStarted.xcf -------------------------------------------------------------------------------- /Images/Resources.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matortheeternal/mxpf/26e834dfe3b00594b7d986ecd48b5f513226907a/Images/Resources.png -------------------------------------------------------------------------------- /Images/Resources.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matortheeternal/mxpf/26e834dfe3b00594b7d986ecd48b5f513226907a/Images/Resources.xcf -------------------------------------------------------------------------------- /Images/Screenshots/ss+(2016-01-16+at+02.33.29).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matortheeternal/mxpf/26e834dfe3b00594b7d986ecd48b5f513226907a/Images/Screenshots/ss+(2016-01-16+at+02.33.29).png -------------------------------------------------------------------------------- /Images/Screenshots/ss+(2016-01-16+at+02.34.36).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matortheeternal/mxpf/26e834dfe3b00594b7d986ecd48b5f513226907a/Images/Screenshots/ss+(2016-01-16+at+02.34.36).png -------------------------------------------------------------------------------- /Images/Screenshots/ss+(2016-01-16+at+02.36.00).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matortheeternal/mxpf/26e834dfe3b00594b7d986ecd48b5f513226907a/Images/Screenshots/ss+(2016-01-16+at+02.36.00).png -------------------------------------------------------------------------------- /Images/Thanks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matortheeternal/mxpf/26e834dfe3b00594b7d986ecd48b5f513226907a/Images/Thanks.png -------------------------------------------------------------------------------- /Images/Thanks.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matortheeternal/mxpf/26e834dfe3b00594b7d986ecd48b5f513226907a/Images/Thanks.xcf -------------------------------------------------------------------------------- /Images/WhatIsMXPF.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matortheeternal/mxpf/26e834dfe3b00594b7d986ecd48b5f513226907a/Images/WhatIsMXPF.png -------------------------------------------------------------------------------- /Images/WhatIsMXPF.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matortheeternal/mxpf/26e834dfe3b00594b7d986ecd48b5f513226907a/Images/WhatIsMXPF.xcf -------------------------------------------------------------------------------- /Images/mxpf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matortheeternal/mxpf/26e834dfe3b00594b7d986ecd48b5f513226907a/Images/mxpf.png -------------------------------------------------------------------------------- /Images/mxpf.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matortheeternal/mxpf/26e834dfe3b00594b7d986ecd48b5f513226907a/Images/mxpf.xcf -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MXPF 2 | MXPF is an xEdit scripting library that provides a variety of functions for the generation of patches. Want to rebalance armor and weapons in Skyrim? Done. Apply a new particle effect to every weather? Easy. Change the configuration of all NPCs? Cake. 3 | 4 | MXPF is comprised a suite of functions that do the work of finding the records you want to patch, identifying/creating a patch file, and copying records to the patch. Where you would normally have to learn to do all of these steps yourself, with MXPF you can accomplish everything you need with a few simple functions. 5 | 6 | ## Installation 7 | To install, simply copy the files in the Edit Scripts folder to xEdit's Edit Scripts folder. 8 | 9 | The Documentation folder contains all the documentation you'll need to get started using MXPF to automate the creation of patches in xEdit. 10 | 11 | The ESPs folder has three plugins for use with the MXPF test suite, you don't need to install them for MXPF to function. 12 | 13 | ## Dependencies 14 | * [mteFunctions](https://github.com/matortheeternal/TES5EditScripts/blob/master/trunk/Edit%20Scripts/mteFunctions.pas): Utility and helper functions for xEdit scripting. Comes packaged with mxpf. 15 | * [jvTest](https://github.com/matortheeternal/jvTest): jvInterpreter testing framework. Comes packaged with mxpf. 16 | * xEdit: Download from [Nexus Mods](http://www.nexusmods.com/skyrim/mods/25859), or [GitHub](https://github.com/TES5Edit/TES5Edit). v3.1.2 or newer is required. The test suite only works with TES5Edit. 17 | * Bethesda game: xEdit supports [Skyrim](http://store.steampowered.com/app/72850), 18 | [Oblivion](http://store.steampowered.com/app/22330), 19 | [Fallout 3](http://store.steampowered.com/app/22300), and 20 | [Fallout New Vegas](http://store.steampowered.com/app/22380) 21 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Mator 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /test plan.txt: -------------------------------------------------------------------------------- 1 | TEST GENERAL 2 | Test DebugMessage 3 | Test DebugList 4 | Test SaveDebugMessages 5 | Test FailureMessage 6 | Test SaveFailureMessages 7 | Test InitializeMXPF 8 | Test DefaultOptionsMXPF 9 | Test FinalizeMXPF 10 | 11 | TEST PATCH FILE SELECTION 12 | Test PatchFileByAuthor 13 | - InitializeMXPF not called 14 | -> Verify mxPatchFile not assigned 15 | - File exists 16 | -> Verify mxPatchFile 17 | -> Verify mxPatchFile's author 18 | - File does not exist 19 | -> Verify mxPatchFile is unassigned 20 | - Multiple files 21 | -> Should choose one highest in the user's load order 22 | 23 | Test PatchFileByName 24 | - InitializeMXPF not called 25 | -> Verify mxPatchFile not assigned 26 | - File exists 27 | -> Verify mxPatchFile 28 | 29 | TEST FILE SELECTION 30 | Test SetExclusions 31 | - InitializeMXPF not called 32 | -> Verify mxFileMode = 0 33 | - InitializeMXPF called 34 | -> Verify mxFileMode = 1 35 | -> Verify mxFiles = our files 36 | 37 | Test SetInclusions 38 | - InitializeMXPF not called 39 | -> Verify mxFileMode = 0 40 | -> InitializeMXPF called 41 | -> Verify mxFileMode = 2 42 | -> Verify mxFiles = our files 43 | 44 | TEST RECORD PROCESSING 45 | Test LoadRecords 46 | - InitializeMXPF not called 47 | -> Verify it doesn't cause an exception 48 | - mxPatchFile not assigned 49 | -> Verify it doesn't cause an exception 50 | - mxPatchFile assigned 51 | -> Verify mxMasters.Text 52 | -> Verify mxRecords.Count 53 | - Verify exclusion mode works 54 | -> Verify mxMasters.Text 55 | -> Verify mxRecords.Count 56 | - Verify inclusion mode works 57 | -> Verify mxMasters.Text 58 | -> Verify mxRecords.Count 59 | - Test mxLoadMasterRecords 60 | -> Verify mxRecords.Count 61 | - Test mxLoadOverrideRecords 62 | -> Verify mxRecords.Count 63 | - Test mxLoadWinningOverrides 64 | -> Verify mxRecords.Count 65 | 66 | Test LoadChildRecords 67 | - InitializeMXPF not called 68 | -> Verify it doesn't cause an exception 69 | - mxPatchFile not assigned 70 | -> Verify it doesn't cause an exception 71 | - mxPatchFile assigned 72 | -> Verify mxMasters.Text 73 | -> Verify mxRecords.Count 74 | - Verify exclusion mode works 75 | -> Verify mxMasters.Text 76 | -> Verify mxRecords.Count 77 | - Verify inclusion mode works 78 | -> Verify mxMasters.Text 79 | -> Verify mxRecords.Count 80 | - Test mxLoadMasterRecords 81 | -> Verify mxRecords.Count 82 | - Test mxLoadOverrideRecords 83 | -> Verify mxRecords.Count 84 | - Test mxLoadWinningOverrides 85 | -> Verify mxRecords.Count 86 | 87 | Test MaxRecordIndex 88 | - InitializeMXPF not called 89 | -> Verify it returns -1 90 | - InitializeMXPF called 91 | -> Verify it returns mxRecords.Count - 1 92 | 93 | Test GetRecord 94 | - InitializeMXPF not called 95 | -> Verify it returns nil 96 | - LoadRecords not called 97 | -> Verify it returns nil 98 | - No records 99 | -> Verify it returns nil 100 | - Invalid index 101 | -> Verify it throws an exception 102 | - Valid index 103 | -> Verify it returns the correct record 104 | 105 | Test RemoveRecord 106 | - InitializeMXPF not called 107 | -> Verify it throws an exception 108 | - LoadRecords not called 109 | -> Verify it throws an exception 110 | - mxRecords.Count = 0 111 | -> Verify it throws an exception 112 | - Invalid index 113 | -> Verify it throws an exception 114 | - Valid index 115 | -> Verify it removes the record from mxRecords 116 | 117 | TEST RECORD PATCHING 118 | Test AddMastersToPatch 119 | - InitializeMXPF not called 120 | -> Verify it throws an exception 121 | -> Verify mxMastersAdded is false 122 | - LoadRecords not called 123 | -> Verify it throws an exception 124 | -> Verify mxMastersAdded is false 125 | - mxPatchfile not assigned 126 | -> Verify it throws an exception 127 | -> Verify mxMastersAdded is false 128 | - mxPatchfile assigned 129 | -> Verify mxMastersAdded is true 130 | -> Verify the patch file has the new masters 131 | 132 | Test CopyRecordToPatch 133 | - InitializeMXPF not called 134 | -> Verify it throws an exception 135 | -> Verify mxCopyCalled is false 136 | - LoadRecords not called 137 | -> Verify it throws an exception 138 | -> Verify mxCopyCalled is false 139 | - mxPatchFile not assigned 140 | -> Verify it throws an exception 141 | -> Verify mxCopyCalled is false 142 | - No records 143 | -> Verify it throws an exception 144 | -> Verify mxCopyCalled is false 145 | - Invalid index 146 | -> Verify it throws an exception 147 | -> Verify mxCopyCalled is false 148 | - Valid index 149 | -> Verify mxCopyCalled is true 150 | -> Verify mxMastersAdded is true 151 | -> Verify the record is added to mxPatchRecords 152 | -> Verify the record is present in the patch file 153 | - mxSkipPatchedRecords 154 | -> Verify mxCopyCalled is false 155 | -> Verify mxMastersAdded is false 156 | -> Verify the record isn't added to mxPatchRecords 157 | -> Verify the record isn't present in the patch file 158 | 159 | Test CopyRecordsToPatch 160 | - InitializeMXPF not called 161 | -> Verify it throws an exception 162 | -> Verify mxCopyCalled is false 163 | - LoadRecords not called 164 | -> Verify it throws an exception 165 | -> Verify mxCopyCalled is false 166 | - mxPatchFile not assigned 167 | -> Verify it throws an exception 168 | -> Verify mxCopyCalled is false 169 | - No records 170 | -> Verify it throws an exception 171 | -> Verify mxCopyCalled is false 172 | - mxSkipPatchedRecords 173 | -> Verify mxCopyCalled is true 174 | -> Verify mxMastersAdded is true 175 | -> Verify mxPatchRecords.Count 176 | -> Verify records are in patch file 177 | - mxCopyWinningOverrides 178 | -> Verify the record copied is the winning override 179 | -> Verify when winning override is in a file not in the file selection 180 | 181 | Test MaxPatchRecordIndex 182 | - InitializeMXPF not called 183 | -> Verify it raises an exception 184 | - InitializeMXPF called 185 | -> Verify it returns mxPatchRecords.Count - 1 186 | 187 | Test GetPatchRecord 188 | - InitializeMXPF not called 189 | -> Verify it returns nil 190 | - No records copied 191 | -> Verify it returns nil 192 | - No records 193 | -> Verify it returns nil 194 | - Invalid index 195 | -> Verify it throws an exception 196 | - Valid index 197 | -> Verify it returns the correct record 198 | 199 | TEST MACROS 200 | Test SetFileSelection 201 | - Mode false 202 | -> Verify in inclusion mode 203 | -> Verify file selection is loaded into MXPF 204 | - Mode true 205 | -> Verify in exclusion mode 206 | -> Verify file selection is loaded into MXPF 207 | Test MultiLoad 208 | - No signatures 209 | -> Verify no records are loaded 210 | - One signature 211 | -> Verify records are loaded 212 | - One signature pair 213 | -> Verify records are loaded 214 | - Multiple signatures and signature pairs 215 | -> Verify records are loaded --------------------------------------------------------------------------------