Introduction and OverviewThe NWN Toolset is an amazing tool, but it has some serious limitations. Generally speaking, you have to work with only one resource at a time - one area, one creature, one trigger, and so on. You can't open multiple windows, so you can't easily edit even a handful of things at once.
Happily, there IS a tool that allows you to do work with multiple resources at once - Moneo, a standalone tool written by dragonsong for testing letoscripts on. With it, you can execute letoscripts outside the nwn environment, using the letoscript language to maniuplate just about any file type associated with NWN. We have used it to great effect on Higher Ground over the years, and this tutorial will attempt to pass what we've learned on to you. It can also be used to do mass servervault edits, but for now, I'm going to focus on things relating to the module itself.
First, I'll look at how to use Moneo to data mine - to get data from your module, often from hundreds or thousands of objects - things that would take inordinate amounts of time to data mine by opening them individually in the toolset. Once I've covered that, I'll explain how to take the next step - altering the data you're looking at, even on a very large scale. Lastly, I'll touch on other, even more advanced applications of Moneo, using our documentation system, written by acaos, to demonstrate just how versatile a tool this is.
FilesBefore all of that, though, you'll need the tools we'll be working with. I'll be providing links from my own Dropbox account, as well as the original links, so that the links will be good for as long as possible.
First up, we have Moneo itself. All you need is the one .exe file. I keep it in its own folder on my desktop, along with all the scripts we run with it, and the files we plan to edit with it. I suggest you make such a folder now, and plop this into it. You only need one of the following two links - a rar'd version (you'll need an archiving program like winrar or
7zip), and the original sourceforge link (the sourceforge link comes with source and other files not necessary to our purpose). I would suggest the rar'd exe, if you want to keep things simple, as it's the only file you absolutely need for the purposes of this tutorial.
Moneo .exe fileMoneo on SourceforgeNext up, you need some way to view files, so that you can find the names of the fields you'll be datamining or editing, because those names are used in the scripts you'll have to write. If you don't already have a gff viewer (that's generic file format, for those of you who care), you can use Alia, another gem of a program by dragonsong. It allows you to pull up all the various kinds of files we'll be working with and view them in an intuitive format. I'm again hosting a version myself, in order to preserve links as long as possible, but I would suggest downloading straight from sourceforge, as the two are identical. As for where to download it, anywhere is fine, but I would suggest NOT putting it in your Moneo folder, to keep it free of clutter. I have it in another folder on my Desktop called NWN Programs (in which is my Moneo folder, as well as my Alia folder, and a few others):
Alia on SourceforgeAlia from my DropboxLast of the files for you to download is this rar, which contains dozens of working letoscripts. They've all been used successfully while working on Higher Ground in the past, and are an invaluable learning resource. Acaos wrote around 80% of them, and I've used them myself as a learning tool. If you already have a working familiarity with Moneo, this is probably the only thing you'll want from this thread. I'll be drawing from these scripts as examples. I keep these in a subfolder of the Moneo folder, and copy them up to that folder when I want to make a new letoscript to run with Moneo:
Letoscripts to Run with MoneoSetupI'm going to assume that you're able to unrar files, and can install Alia on your own - it's very straightfoward. Moneo requires no installation other than unrarring it. Other than that, and the files above, here are a few more notes.
1) We'll be working in dos prompt, to run Moneo from command line. I am assuming you know how to open a command prompt on your computer, and how to navigate to the Moneo folder you made. If not, feel free to ask questions, but you should also be able to find an operating-system specific guide to doing this with a quick search of the interwebs. On Windows 7, you can just click the Start Menu button, type in 'command', and select the Command Prompt from the list of programs that results.
2) ALWAYS MAKE BACKUPS. There is no excuse for not making regular backups of every file you work on, and this includes your module. There is a very small chance that something will go awry during an edit, with corruption of a file as a possible outcome. Unless you want to risk losing things, use an archiver (like 7zip, linked above) to make backups of your module before you Moneo it. Aside from archived backups going back years, I also keep copy of the file I'm working on in a Backup folder on my Desktop, mostly for convenience's sake.
3) You may want to add the Moneo .exe to your system path, so that you can use the command from any folder, rather than just the folder you dropped it in. There's a simple guide to doing so here, for Windows 7, though again, a quick search of the internet will turn one up for whatever OS you're on:
Adding Files to your System Path4) If you're not comortable with command prompts, you can also write text files with the extension .bat to run moneo scripts by simply clicking on the .bat file. I'll elaborate in the first example, just below.
ExamplesExample #1 - Datamining Areas and How to Run MoneoFor this first example, we'll look at how to loop through the areas in a module.
1) Before we look at the script, though, take the module you want to use, and copy it. Paste one copy into your moneo folder, and another into your backup folder. You CAN specify a path to a file to edit with Moneo, but we won't in this example, so Moneo will look for the file name specified in the same folder.
2) Now, let's make the script we're going to run. Moneo doesn't have a compiler, so it can be hit-or-miss at times (though there ARE some error messages). Open a text file with notepad or your editor of choice, and paste this code into it. If you like you can copy it straight from the countplaces.ls file in the rar of letoscripts linked above.
%mod = 'YOUR_MOD_NAME_HERE.mod';
for (%mod['*.git']) {
$count = 0;
for (/{'Placeable List'}) {
$count++;
}
print $count, "\\n";
}
for (%mod['*.are']) {
$tiles = 0;
for (/{'Tile_List'}) {
$tiles++;
}
print /Tag, " ", /Name, " ", /ResRef, " ", $tiles, "\\n";
}
Now replace YOUR_MOD_NAME_HERE with the name of your module, making sure to leave the .mod at the end. Now save this file in your Moneo folder as countplaces.ls (NOT a .txt file extension). You're now ready to run this code with Moneo.
3) Before we run this code, let's take a look at it. The mod specification is straightfoward enough. You can see we are looping through two different file types here - gits and areas. Areas are actually composed of 3 files each, and you may need to use Alia to figure out which file or files among them have the data you want in them.
Likewise, what this code does is fairly transparent - it just uses different syntax and rules than nwscript. You can see that it loops through the placeable list, incrementing a counter for each placeable in that list, for each area, and then prints the number with a newline after each - more on printing in a moment. Then, it looks in the second file type, and prints the tag, name, resref, and tile count of each area. The concatenation of strings is done with commas, as you can see, and those blank spaces in between the quotes are tabs, so that the printout can be put directly into a spreadsheet. This allows us to put the counts right next to the tag/res/name/tile info in a spreadsheet for further manipulation.
4) Now that we have a grasp of what we're doing, let's do it. Open a dos prompt, and navigate to the Moneo folder. Type the following line:
moneo countplaces.ls > countplaces.txt
That command tells the computer to execute the moneo program, which then looks for the countplaces.ls script and executes it. The last bit, > countplaces.txt, tells the computer to output any outputs to the countplaces.txt file, which will be created in the Moneo folder (you can name it whatever you like, it doesn't have to match the letoscript name). Go ahead and hit enter. After a brief time, you should now have that .txt file in your Moneo folder with the results you wanted.
Example #2 - Datamining Creature VariablesThis example gets slightly more complex, since it looks through the lists storing variable data on each creature, and prints it where appropriate. It means a little more looping, and requires an understanding of how list items are specified.
We'll start with the code. I'm not going to have you run this one, since it's unlikely you have any critters in your module with this particular variable name set on them. Here it is:
%mod = 'Path of Ascension CEP Legends.mod';
for (%mod['*.utc']) {
$val = 0;
for (/VarTable) { $val = /~/Value if /~/Name eq 'AbilityAB'; }
if ($val > 0) {print /TemplateResRef, " ", /FirstName, " ", /LastName, " ", $val, "\\n";}
}
You can see we again start by specifying the mod we're working with. We then do a loop, this time through all of the mod's utc files (the 'c' is for 'creature'). Here we see one of the ways to do an if statement in moeno, with it following the var setting. You can also do nwscript-style if statemnets. String equalities are checked with 'eq' instead of ==, however (number equalities are the same as nwscript). We also see how sublists are navigated. VarTable is a list of all the vars on the object, and each var has a sublist with Name and Value fields, among others - check Alia if you're curious. We specify these sublist fields by adding /~ in front of the normal foward slash field designator, as you can see above. Here, we look for a variable named AbilityAB, and if found, set $val equal to its value. If $val is set, we then print it, along with the resref and name of the creature. Make sense?
Example #3 - Looping only some FilesThis one is like the loops above, but restricts the loop to only files with a certain prefix:
%mod = 'Path of Ascension CEP Legends.mod';
for (%mod['ely_*.utc']) {
print /TemplateResRef, " ", /ChallengeRating, " ", /FirstName, " ",/Race, "\\n";
}
Pretty standard wildcard use. This limits the data dump to only creatures whose resref is prefixed by ely_.
Example #4 - Editing away Comments from CreaturesHere, we're going to actually make changes to the module. This entails one major difference - we need to tell Moneo to save and close the module. We do so with the following two lines:
%mod = '>';
close(%mod);
Here's the full code:
%mod = 'Path of Ascension CEP Legends.mod';
for (%mod['uro*.utc']) {
/Comment = "";
}
%mod = '>';
close(%mod);
As you can see, we're still declaring the mod file the same way, and looping through the creature files the same way. The only new thing is that instead of checking a fields value, we're setting it. As you can see, we are setting the creature's Comment field to a blank string. This is handy if you have stray comments on your monsters you don't need taking up unnecessary space in your module (the reason acaos wrote this script in the first place).
Example #5 - Removing Prefixes from Item NamesHere's the code:
%mod = 'Path of Ascension CEP Legends.mod';
for (%mod['*.uti']) {
if (/PaletteID == 3 && substr(/LocalizedName, 0, 4) eq "ABO ") {
/PaletteID = 1;
/LocalizedName = substr(/LocalizedName, 4, 100);
print /TemplateResRef, " '", /LocalizedName, "'\\n";
}
}
%mod = '>';
close(%mod);
Here, you can see how to check only items in a certain palette category. This also highlights letoscript's substring command, showing how to check a substring, and how to set with it. Note that here we're also printing out something, so you would want to output to a text file with >, as described in example #1. This is a handy thing to do to make sure your letoscirpts are doing what you want them to.
Example #6 - Editing Item Property SubtypesCode first:
%mod = 'Path of Ascension CEP Legends.mod';
for (%mod['*.uti']) {
if (/StackSize > 10) {
for (/PropertiesList) {
if (/~/PropertyName == 82 && /~/Subtype == 125) {
/~/Subtype = 135;
print /TemplateResRef, " ", /StackSize, "\\n";
}
}
}
}
%mod = '>';
close(%mod);
Here we see how to edit item properties - specifically, we're changing the subtype from 125 (onhit unique power) to 135 (onhit intelligent weapon) on all item properties of 82 (onhitcast), on all items of stacksize greater than 10. Nothing you haven't seen the examples above, except the sublist setting.
Example #7 - Renaming all Static Placeables in your Module
%mod = 'Path of Ascension CEP Legends.mod';
for (%mod['*.git']) {
for (/{'Placeable List'}) {
if (/~/Static > 0) {
$name = /~/Appearance;
if ($name < 10) {
$name = "ZZ_0000" + $name;
} elsif ($name < 100) {
$name = "ZZ_000" + $name;
} elsif ($name < 1000) {
$name = "ZZ_00" + $name;
} elsif ($name < 10000) {
$name = "ZZ_0" + $name;
} else {
$name = "ZZ_" + $name;
}
/~/LocName = $name;
/~/PortraitId = 515;
}
}
}
%mod = '>';
close(%mod);
This script, aside from being generally handy for renaming all your areas' static places to a standard naming convention (making it easy to pick out script/player interactables), shows how to do else if logic in letoscript. You can, however, also do 'else if' - it's a matter of preference. It sets the name according to a naming convention, and changes the portrait.
Example #8 - Adding a Variable to an Item that may not Have One
%mod = 'Path of Ascension CEP Legends.mod';
for (%mod['locset*.uti']) {
add /VarTable, type => gffList unless /VarTable;
add /VarTable/Name, type => gffString, value => "ITR";
add /VarTable/[_]/Type, type => gffDword, value => 1;
add /VarTable/[_]/Value, type => gffInt, value => 4194304;
}
%mod = '>';
close(%mod);
This is a little tricky, because you only want to add a VarTable list if there isn't one - that's what the first line in the for loop checks. From there, it shows how to add a new variable to the item, having ensured there is a VarTable to add to. Note that it does NOT check to see if the var is already set, because this was a brand new variable for us - you might need to do that, in some circumstances, using logic from previous examples.
Example #9 - Putting it all TogetherHere's an example of the most complex letoscript I've written to date. It involved editing all manner of things on certain factions of creatures in our module, in order to offset a nerf we'd recently done to bard and curse song. It highlights the fact that you can generally do whatever you need to in letoscript, utilizing typical nwscript logic, once you understand the syntax. There's not much new in the below code, but I'll highlight a few things after it
%mod = 'Path of Ascension CEP Legends.mod';
for (%mod['*.utc']) {
$fac = /FactionID;
if ($fac == 65 ||
$fac == 66 ||
$fac == 67 ||
$fac == 72 ||
$fac == 73 ||
$fac == 74 ||
$fac == 77) {
$abilitydrop = 10;
$acdrop = 7;
$savedrop = 3;
$str = /Str;
$dex = /Dex;
$abilitymoddrop = int($abilitydrop/2);
$skill = 0;
$vardis = 0;
$varpar = 0;
$dis = 0;
$par = 0;
$skillfound = 0;
$dexdrop = 0;
if (($dex > $str) || (($str-$dex) < ($abilitydrop+1))) {
if (/Dex > ($abilitydrop+2)) {
/Dex = /Dex - $abilitydrop;
$dexdrop = $abilitymoddrop;
/refbonus = /refbonus + $abilitymoddrop;
$par = /SkillList/[10]/Rank;
if ($par > 0) {
if ($par < (100-$abilitymoddrop)) {
/SkillList/[10]/Rank = /SkillList/[10]/Rank + $abilitymoddrop;
} else {
for (/VarTable) {
if (/~/Name eq "Skill") {
$varpar = int(/~/Value / 100) % 10;
$skillfound = 1;
if ($varpar < 9) {
/~/Value = /~/Value + 100;
if ($abilitymoddrop < 5) {
/SkillList/[10]/Rank = /SkillList/[10]/Rank - (5-$abilitymoddrop);
}
} else {
print /TemplateResRef, " ", /FirstName, ": parry offset attempted but var too high", "\\n";
}
}
}
if ($skillfound != 1) {
add /VarTable, type => gffList unless /VarTable;
add /VarTable/Name, type => gffString, value => "Skill";
add /VarTable/[_]/Type, type => gffDword, value => 1;
add /VarTable/[_]/Value, type => gffInt, value => 100;
print /TemplateResRef, " ", /FirstName, ": parry offset added Skill var", "\\n";
if ($abilitymoddrop < 5) {
/SkillList/[10]/Rank = /SkillList/[10]/Rank - (5-$abilitymoddrop);
}
}
}
}
#print /TemplateResRef, " ", /FirstName, ": dex also edited", "\\n";
} else {
print /TemplateResRef, " ", /FirstName, ": dex edit attempted but dex too low", "\\n";
}
}
if ($dexdrop != 0) {
$acdrop = $acdrop-$dexdrop;
}
if (/NaturalAC >= $acdrop) {
if ($acdrop != 0) {
/NaturalAC = /NaturalAC - $acdrop;
}
#print /TemplateResRef, " ", /FirstName, ": natural ac lowered by ", $acdrop, "\\n";
} else {
$acdex = int($acdrop*2);
if ($dex > ($acdex + 2)) {
/Dex = /Dex - $acdex;
/refbonus = /refbonus + $acdrop;
$par = /SkillList/[10]/Rank;
if ($par > 0) {
if ($par < (100-$acdrop)) {
/SkillList/[10]/Rank = /SkillList/[10]/Rank + $acdrop;
} else {
for (/VarTable) {
if (/~/Name eq "Skill") {
$varpar = int(/~/Value / 100) % 10;
$skillfound = 1;
if ($varpar < 9) {
/~/Value = /~/Value + 100;
if ($acdrop < 5) {
/SkillList/[10]/Rank = /SkillList/[10]/Rank - (5-$acdrop);
}
} else {
print /TemplateResRef, " ", /FirstName, ": parry offset attempted from second dex edit offsetting ac, but var too high", "\\n";
}
}
}
if ($skillfound != 1) {
add /VarTable, type => gffList unless /VarTable;
add /VarTable/Name, type => gffString, value => "Skill";
add /VarTable/[_]/Type, type => gffDword, value => 1;
add /VarTable/[_]/Value, type => gffInt, value => 100;
print /TemplateResRef, " ", /FirstName, ": parry offset, attempted from second dex edit offsetting ac, added Skill var", "\\n";
if ($abilitymoddrop < 5) {
/SkillList/[10]/Rank = /SkillList/[10]/Rank - (5-$acdrop);
}
}
}
}
#print /TemplateResRef, " ", /FirstName, ": ac too low to edit but dex used to offset it", "\\n";
} else {
print /TemplateResRef, " ", /FirstName, ": ac too low to edit and dex too low to offset it", "\\n";
}
}
#Save variable 9NMDfrwFRW - only set in hiv_, and there only D set - all saves there subtractable, so no need to check for var before adding
$fort = /fortbonus;
$ref = /refbonus;
$will = /willbonus;
$diff = 0;
$odd = 0;
$fortinc = 0;
$refinc = 0;
$willinc = 0;
$savevar = 0;
if ($fort >= $savedrop) {
/fortbonus = /fortbonus - $savedrop;
} else {
$diff = $savedrop - $fort;
$odd = $diff % 2;
$fortinc = int($diff / 2) + $odd;
if ($odd == 1) {
/fortbonus = 1;
}
$savevar = $fortinc * 100000;
}
if ($ref >= $savedrop) {
/refbonus = /refbonus - $savedrop;
} else {
$diff = $savedrop - $ref;
$odd = $diff % 2;
$refinc = int($diff / 2) + $odd;
if ($odd == 1) {
/refbonus = 1;
}
$savevar += ($refinc * 10000);
}
if ($will >= $savedrop) {
/willbonus = /willbonus - $savedrop;
} else {
$diff = $savedrop - $will;
$odd = $diff % 2;
$willinc = int($diff / 2) + $odd;
if ($odd == 1) {
/willbonus = 1;
}
$savevar += ($willinc * 1000);
}
if ($savevar != 0) {
add /VarTable, type => gffList unless /VarTable;
add /VarTable/Name, type => gffString, value => "Saves";
add /VarTable/[_]/Type, type => gffDword, value => 1;
add /VarTable/[_]/Value, type => gffInt, value => 0;
#for some reason, cannot set value => $savevar directly, must set after
for (/VarTable) {
if (/~/Name eq "Saves") {
/~/Value = $savevar;
}
}
#print /TemplateResRef, " ", /FirstName, ": Save variable added to decrease saves: ",$savevar, "\\n";
}
if (/Str > ($abilitydrop+2)) {
/Str = /Str - $abilitydrop;
$dis = /SkillList/[1]/Rank;
if ($dis < (100-$abilitymoddrop)) {
/SkillList/[1]/Rank = /SkillList/[1]/Rank + $abilitymoddrop;
} else {
for (/VarTable) {
if (/~/Name eq "Skill") {
$vardis = /~/Value % 10;
$skillfound = 1;
if ($vardis < 9) {
/~/Value = /~/Value + 1;
if ($abilitymoddrop < 5) {
/SkillList/[1]/Rank = /SkillList/[1]/Rank - (5-$abilitymoddrop);
}
} else {
print /TemplateResRef, " ", /FirstName, ": discipline offset attempted but var too high", "\\n";
}
}
}
if ($skillfound != 1) {
add /VarTable, type => gffList unless /VarTable;
add /VarTable/Name, type => gffString, value => "Skill";
add /VarTable/[_]/Type, type => gffDword, value => 1;
add /VarTable/[_]/Value, type => gffInt, value => 1;
#print /TemplateResRef, " ", /FirstName, ": discipline offset added Skill var", "\\n";
if ($abilitymoddrop < 5) {
/SkillList/[1]/Rank = /SkillList/[1]/Rank - (5-$abilitymoddrop);
}
}
}
} else {
print /TemplateResRef, " ", /FirstName, ": str too low to edit", "\\n";
}
}
}
%mod = '>';
close(%mod);
What's new in the above:
a) you see how to cast a variable as a type - this line casts the result as an int:
$abilitymoddrop = int($abilitydrop/2);
you see how to set a skill value (skill 10, per the 2da, is Parry):
/SkillList/[10]/Rank = /SkillList/[10]/Rank - (5-$abilitymoddrop);
Here, we didn't even have to know what the skill rank was to modify it. You can figure out the various lists by looking at them in Alia, per the Files section.
c) you see how comments are written in letoscript (using # instead of //, or /* */)
d) You also see how to use print to debug your letoscripts and figure out what they're doing, though that's hardly unique to letoscript.
e) You also see how you can check for a variable of a given name before adding one of that name to the VarTable - see the $skillfound variable in the code above.
f) Lastly, you see that you can use the typical math operators normally, like / and %:
$varpar = int(/~/Value / 100) % 10;
Example #10 - Advanced ApplicationsThis last one I'm providing to show what a versatile tool Moneo can be. Acaos set up a document repository with txt file structures set up for a unix program to operate on. Using it, we generate new forum documentation AND a letoscript, which we then run using Moneo to generate journal entries containing the same documentation ingame, saving us a lot of duplicative work in documenting our edits to our module. I'm linking it as a standalone file, because it's about four thousand lines.
Journal Entry Builder LetoscriptAside from showing how to manipulate complex list structures, it shows how to work with journal entries using moneo, and gives you some idea of the near-limitless possibilities Moneo has to offer.
That concludes the main body of the tutorial, at least for now. Please feel free to ask any and all questions relating to Moneo.
Funky
Modifié par FunkySwerve, 09 novembre 2011 - 06:42 .