Jump to content

Tutorial: Everything About LRR Level Scripting

Recommended Posts


EDIT: This version isn't the most up to date. For the most up to date version, visit the Knowledgebase:


It was moved over to there because it meant I could edit sections with ease, as opposed to having to edit the entire post.



Wall of Text has awoken!


I'd like to make this as a guide but I can't find the Create Guide button. :S Ah well, a forum post is not a problem! I've been working on this on and off for the past half-year or so, as the LRR scripter is really darn powerful but badly-documented. DDI did a very bad job with level design (especially slug scripts) - we can do so much more than GetCrystalsCurrentlyStored > 49 ? SetLevelCompleted ! The intention was to create a guide that explained to experienced programmers (even if they're only experienced in Scratch!) how to use the funky LRR scripting, and also guide new people who don't know a single programming language to be able to make something for their level.

Oh, and if you have debug keys enabled you can hit F12 to disable the Nerps scripter (= this stuff) until you hit F12 again. A nice little NoNerps will pop up in the top left corner to let you know Nerps is disabled.



1. File Formatting

2. Basic Stuff

3. Toying with Timers

4. Toying with Variables

5. Labelling

6. Messaging

7. Fun Things!

7a. Basic Win/Loss

7b. Monster Invasions

7c. Slug Spam

8. Highlights from Karl's Interview Thread

9. More Research



--- File Formatting ---


TL;DR .nrn and .nrm files are plain text and compile to .npl, which is what LRR reads.


If you poke around a Level folder, you will find two or three files of interest.
something.nrm , something.nrn, something.npl . Usually they're all numbers. Like 04.nrm, 04.nrn, and 04.npl.
The nrn and nrm files are totally equivalent (& you can open them in something like Notepad, for instance). They are the plain text of code. Using the fantastic tools available on RRU, you can turn these into .npl files. These are the code that LRR actually reads. It's no use just making a file though and hoping LRR will pick it up - you need to specify in the Levels section of the CFG the NerpsFile. This needs to point to the npl file. I haven't tried feeding it an nrm, but I don't think it'd go well. Nrm and nrn files have no need to exist, but it's useful to have them in the same folder as your compiled npl - mostly just so that if you want to tweak the script the 'source code' is right there and you don't have to bother with decompilation faff.




--- Basic Stuff ---


TL;DR The scripter loops when it finishes, everything must have CONDITION ? ACTION


Firstly, commenting is done with ; . The compiler will ignore anything after that semicolon and go straight to the next one – so LRR couldn’t care less if it existed, as it will never see it in the .npl file.


; This is a comment and LRR really couldnt care less that it existed

Onto more interesting stuff.


The basic syntax of any line (with two exceptions) is:




When the condition is met, something will happen. If not, the NERPs goes to the next line. A condition could be something obvious like GetOxygenLevel = 0 , or GetMonstersOnLevel = 0. Other operators you can use include > (bigger than), < (smaller than), and != (not equal to). You can use numbers, or another Get function for the other half of the condition. DoSomething could be something like SetLevelCompleted (you'll usually want that somewhere in your script!), setting variables with something like SetR0 10 (more on this later), sending messages with SetMessage 1 1 (also more on this later), spamming slugs with Generate Slugs (again, more on this later),

All Get functions return numbers. GetSlugsOnLevel will just spit back a number that is the number of slugs on the level. GetPoweredDocksBuilt will spit back a number that is how many powered docks are on the level. And so on...


A very basic slug script that you are welcome to use is

GetSlugsOnLevel < GetMonstersOnLevel ? GenerateSlug

which ensures that if there are no slugs and lots of monsters, the game will create slugs until the number of slugs is greater than (or equal to) the number of monsters.


There are some quirks to this condition. Firstly, TRUE ? is a condition that will always execute. FALSE ? , obviously, will never execute and I can think of no reason why you’d use it. Here’s a useless example:

TRUE ? SetLevelCompleted

This is a hilariously useless script as it will immediately cause a victory.


Most commands are written in a nice way. It's not too hard to guess that GetMonstersOnLevel returns a number that is the number of monsters on the level. The curious bit is whether it counts sleeping monsters (it does), bats (never checked but would go for "probably"), small monsters (don't think so if they're generated by a large monster death - placing them manually in the ObjectList.ol might have different results). And it's not hard to guess that GenerateSlug creates a slug - just one slug, it turns out.

Things get more confusing with stuff like SetMessageTimerValues, SetCongregationAtTutorial (congregation of what? I think it's monsters. Does it include slugs? Bats?), and other obscure commands. Your best bet when researching new commands is to a) find an existing DDI script that has these and b) to play around repeatedly with a test map until you can get it to work.


At this point it is well worth mentioning what happens when the scripter runs out of things to script. Unlike ‘proper’ programming languages, which will exit, this scripter just goes straight back to the beginning of the npl file.

To illustrate, take a look at this script. This is all it contains.

 TRUE ? GenerateSlug

This will not just generate one slug. No, it will generate infinite quantities of slugs. Every time it reaches the end of the script, it just goes back to the top again. And makes another slug. And finishes its script, goes back up to the top, and generates another slug. The scripter seems to like to run at a rate certainly higher than 1 per second. Estimates are on 25 runs per second, as that’s how long a ‘time unit’ is defined elsewhere in LRR (see the top of the Lego.cfg file; “times are 25 per second”). This has caused me infinite woes in my slug scripts. I start the level, only to be greeted by A SLIMY SLUG IS INVADING YOUR BASE and massive slowdown. Oddly LRR doesn’t crash when this happens, although sometimes it lags so badly I can’t even move the mouse to quit it. Also the scripter seems to ignore the game speed - it seems to run while the game is paused and still seems to run at the same rate if you are at 300% speed (which can be achieved with debug keys).


Another quirk of the condition requirement, is, oh, say there are 0 monsters on a level and 1 slug on the level when the scripter is running for this tick.

GetMonstersOnLevel ? DoSomething1
GetSlugsOnLevel ? DoSomething2

The scripter sees “GetMonstersOnLevel” as 0. As far as the scripter is concerned, this means FALSE. So DoSomething1 will not execute. GetSlugsOnLevel, on the other hand, is 1; and so DoSomething2 WILL happen. (DoSomething1 and DoSomething2 would need to be proper commands, of course).

You can use this to save a little space, but it’s really not necessary and for the sake of legibility I would highly recommend you write GetMonstersOnLevel = 1 ? DoSomething1. It’s twice as easy to understand.

An exception is GetTutorialBlockIsGround, which looks confusing if written as GetTutorialBlockIsGround 2 = 1 ? and would probably be more legible as GetTutorialBlocksIsGround 2 ? DoSomething. They’re still functionally equivalent.



I mentioned two exceptions to this CONDITION ? DOSOMETHING rule. These are the only two lines that do not need a condition.

Stop stops the scripter and loops it all the way up to the top of the script. You can write TRUE ? Stop if you want. You absolutely must write TRUE ? Stop . I don't know where I got the idea you could write Stop on its own, but it does not work.

Stop is an incredibly useful line when it comes to labels, but it by no means quits the game!

Label: - I will talk about labels a little later. This says “The top of the label is here!” However if you want LRR to always go to a label, you will need TRUE ? :Label (more on this later).
Strictly speaking, comments are a third line that doesn’t need a condition… but they don’t do anything anyway.


Also, LRR has no AND function. It doesn’t have an OR either. Or XOR. Or NAND. It’s only got < , > , = , and != . We can make an impromptu AND function by using labels or using a variable – more on this later.






Toying with Timers


TL;DR Timers are in milliseconds. And they don’t reset on level restart.



We’ve got four timers to play around with. We can get their value using GetTimer0 (or GetTimer1, or GetTimer2, or GetTimer3). Maybe you want a condition to execute after ten seconds have elapsed, so you might write something like this:

GetTimer1 > 10 ? GenerateSlug

Unfortunately, timers are in milliseconds, so that will take 0.010 seconds before the game starts spamming you with slugs – because timers don’t automatically reset when you do something.
Let’s modify that script a bit in light of that:

GetTimer1 > 10000 ? GenerateSlug
GetTimer1 > 10000 ? SetTimer1 0

This will wait 10000 milliseconds ( = ten seconds) before generating a slug and resetting the timer.


Timers have a limit of 32000 ish (haven't bothered to nail it down but my guess would be 32768 = 215 ; powers of 2 are terribly important in computing) – in that if you put bigger numbers in, like

GetTimer1 > 33000 ? GenerateSlug

the compiler will refuse to compile and will start working again when you reduce the number. This might mean you have a maximum of thirty seconds… but you can use a register to count the number of times thirty seconds has ticked over. More on this down below!


The TL;DR mentioned that timers are not reset on restart. Registers (see below) are, but timers are not. This usually isn’t a pain, but if you have a monster or slug spam coming in thirty seconds since they start the level, you can get silly results happening if you restart the level.
There is however a fortunate way around this and that is the very useful function GetObjectiveShowing. This should do all you need:

GetObjectiveShowing = 1 ? SetTimer0 0
GetObjectiveShowing = 1 ? SetTimer1 0
GetObjectiveShowing = 1 ? SetTimer2 0
GetObjectiveShowing = 1 ? SetTimer3 0

This may and probably will break if you open the objective mid-game, but really, who does that?

Another annoying quirk of timers is that they start counting the moment you load the level – even when Chief is still speaking to you. Fortunately, the above script will also handle this, and just reset the timers every time the objective is showing.


I find it hard to find a use for timers. I’m much better with…







Toying with Variables


TL;DR You should be using these. A lot. They're integers.


Variables are half the fun of programming in general, and I certainly would have a great deal more difficulty were they not there. We have eight ‘registers’ (integer variables that can store 0 -> 32000… I think. They might happily store negative values, but I'm unsure why you might want that anyway).  We can find their value by using things like GetR3. We can add things to them by writing AddR1 5, or subtraction using SubR5 3. No, there’s no multiplication or division. A line of a bigger script might look like:

GetR1 < 0 ? SubR1 1

If R1 is bigger than zero, take away 1 from R1. (I have actually used this line somewhere… it does have its uses!)

All variables (not timers!) are set to 0 on level start or restart. You don’t need to manually reset them – and if you say something like

TRUE ? SetR1 0
; rest of script

then because the NERPs scripter likes to just go back to the beginning when it runs out of things to do, R1 will be permanently set to 0 and you’ll get nothing useful (unless that’s what you wanted.)
If you wanted something to run at the beginning of the game, it’s probably just best to use a variable to keep track of it. I say 'probably' because I am yet to toy with this myself. Research awaits!

GetR0 = 0 ? SetDocksLevel 1
GetR0 = 0 ? SetR1 1

This will set the Docks to level 1, then set R1 to 1 such that those two lines will never trigger again. (I thiink.... haven't actually tested this!)

An even better way would be to use GetObjectiveShowing = 1 ? SetPowerStationLevel1, although this may cause shenanigans if the player clicks on “Display Objective” midway through the game. (Though who does that? Once you’ve got your crystal count you’re good to go!). Why is it better? Because we have a maximium of eight registers (this and 4 timers are our only limits) and thus if we can not use a register in favour of anything else we can use that register somewhere else.


I mentioned registers could be used as essentially AND blocks. This is a classic bit of DDI code:

TRUE ? SetR1 0
GetMinifiguresOnLevel = 0 ? AddR1 1
GetToolStoresBuilt = 0 ? AddR1 1
GetR1 = 2 ? SetLevelFail

It’s vital to reset R1 to 0, or else it will just be continually incremented every time it runs – causing you to fail the level if you have no raiders on the level but you still have a Tool Store.
You could also use this is as OR, by saying


or an NAND, by saying


A XOR is probably doable with this setup as well, but I haven't bothered looking into it simply because I haven't had a use for it yet in my scripts. It might be GetR1 = 1 .... perhaps?

I mentioned you could combine a timer with a register to record even longer times. Take a look at this:

GetTimer1 > 30000 ? AddR1 1
GetTimer1 > 30000 ? SetTimer1 0

GetR1 < 4 ? Stop
; do interesting things, preferably involving this line:
TRUE ? SetR1 0

So when Timer1 reaches 30000, it will add 1 to R1 and then reset itself. When it’s done this four times (120s => two minutes), it will no longer execute the Stop and will now start doing interesting things! Preferably reset R1 to 0 somewhere in there, or else it’ll just execute immediately afterwards later.








TL;DR Labels are like bookmarks. “Go to here.”



Labels, combined with variables, allow you to do lots of fun things. Remember how those slug scripts would just infinitely generate slugs and lag your game to bits (try it out!). Well, there’s a way to make interesting slug scripts (or anything!) and that is labels.

A label is not quite as advanced as a loop in other programming languages. They’re the good ol’ GOTO function. They’re ways for you to tell the NERPS “Jump execution to here.” When the label finishes, the scripter will not go back to where it was told to go to that label. No, it’ll just continue onwards. If it hits the end of the script, it’ll go back up. If it bumps into a new label, it’ll just keep on going.


As an analogy, labels are like bookmarks. You can say “Go to the bookmark called STUFF.” The scripter then goes over to STUFF and continues reading the book from there. If it finds another bookmark on its way through the book, it’ll ignore it and just keep reading.


Why did I bother with a duplicate explanation with this analogy? Because the ‘bookmark’ analogy explains a lot of the silly stuff that labels do…

Remember how I said that labels didn’t need a CONDITION ? in front of them? Well, that’s only half true. Defining labels needs no condition, but going to labels needs one.
Labels are defined with a colon in front of them, like


and to go to a label, you use


If you were to write only


the game will entirely ignore that and proceed to the next line.


Here’s a silly example:

GetOxygenLevel < 10 :YourO2IsLow

TRUE ? GenerateSlug

TRUE ? GenerateSlug
TRUE ? :Continue

While this might seem silly and basic (because it is), I have used this kind of syntax before. I'll go through several interesting things about labels here.

Firstly, going to labels, like anything else, needs a condition. If you want it to go to another label all the time, you still need a condition; condition TRUE ? .

Secondly, there’s a Stop up the top. This is because I don’t want Continue: nor YourO2IsLow: to run every single time. A Stop ensures that those don’t run by accident. (Stop is the other statement that requires no CONDITION ? in order to run).

Thirdly, you’ll notice that labels can be in any order; just like bookmarks. “Jump to this bookmark, skipping over this other bookmark on your way through.”

Fourthly, if you have a keen eye for how the NPL scripter works, you’ll notice that as soon as your oxygen level gets less than 10 you will be spammed with two slugs every frame.

Another useful note is that while you only have eight registers to play around with, you can have as many labels as you like.


Instead of that useless example, here is a label being used properly. This is an extract from Time Raiders, whereby you can generate Ore by building Docks.

GetTimer1 < 10000 ? :Skip
TRUE ? SetTimer1 0
TRUE ? AddStoredOre GetPoweredDocksOnLevel

; rest of script

If Timer 1 is smaller than 10,000 -> ten seconds, I tell the scripter to go to Skip. So it ignores all that stuff.
When Timer1 gets bigger than 10000, the action of going to Skip will no longer happen. So it continues as normal. It resets the timer and adds some Ore. On the next run, the timer has just been reset and is probably something puny, like 5 milliseconds. So it goes to Skip again.
(Curiously this script generates two ore per powered dock per ten seconds. :S)

You could have used no labels and written something like this as well:

GetTimer1 > 10000 ? AddStoredOre GetPoweredDocksOnLevel
GetTimer1 > 10000 ? SetTimer1 0

Perfectly valid!

Let’s take a look at a classic DDI script that turns up everywhere:

TRUE ? SetR1 0
GetMinifiguresOnLevel = 0 ? AddR1 1
GetToolStoresBuilt = 0 ? AddR1 1
GetR1 = 2 ? SetLevelFail

If you’re out of raiders, and lost your tool store and can’t teleport any more down, you’ve obviously failed the level. (Yes, even if you have a powered Teleport Pad; you won’t be able to teleport raiders down as the Tool Store is a ‘dependency’ for teleporting a Rock Raider).

This script is pretty good, but it has the disadvantage that it uses a register and it would be nice not to use up one of those. So we could rewrite it using labels:

; rest of script
GetMinifiguresOnLevel = 0 ? :CheckForFailure
; preferably as little code here as possible

GetToolStoresBuilt = 0 ? SetLevelFail

The way this script runs should be obvious. If no raiders, it’ll execute the other check for failure. If no toolstores, it'll then fail the level. Don't forget that Stop!

I’ve mentioned that there should be as little code as possible below the GetMinifiguresOnLevel = 0 ? :CheckForFailure. It’s not much of a problem in this example, but if that condition is activating a lot of the time then it will always be jumping to that label – and never executing the code below it.

Fortunately there is a sensible way around this! And it involves… more labels.

; rest of script
GetMinifiguresOnLevel = 0 ? :CheckForFailure
; some more code

GetToolStoresBuilt = 0 ? SetLevelFail
TRUE ? :Continue

This time, if you have no minifigures but a Tool Store or two, the scripter will hop back up to Continue. It is vitally important that that TRUE ?  is there, as that was causing me a load of headaches with one of my more massive scripts.

Yet another way of writing this would be using labels to skip code. This happens a lot in DDI scripts with confusing names liked SkippedSkip. DDI levels are terribly written and you can all do much better than they do, even if you have no programming knowledge at all.

Anyway, a Skip example. Providing you always have raiders on the level, it’ll always skip the next check for failure.

GetMinifiguresOnLevel != 0 :Skip
GetToolStoresBuilt = 0 ? SetLevelFail

Much condensed, but the condition now being if you are NOT going to fail the level, do something can bend your head a bit. Either way, it's a nice way to save a variable!

There seems to be no limit on the number of labels you can use, whereas you're clearly restricted to eight registers.... thus saving a register in favour of even ten labels can be worthwhile!







TL;DR SetMessage NUM 1 and don't forget SetMessagePermit 0


You know how

TRUE ? GenerateSlug

will spam you with slugs?

Can you guess what

TRUE? SetMessage 1 1

will do?

The answer is not spam messages. No, Chief will happily speak message 1 and then stop talking. The text will be there for eternity down the bottom, overwriting the An Energy Crystal Has Been Found / Ore Has Been Found / A Unit Has Been Upgraded but really who cares about those? (Don't worry - we'll find a way to banish the text in a little while!)



SetMessage has two numbers after it! Ooooh! This really shouldn't scare you at all as it needs to know two things. It's much easiest to explain the second number. If it is 0, a little "Next" arrow will appear like it does in the tutorials. I don't know how to make use of this arrow - if you want to research it, check out what the tutorial levels do. If it's 1, don't display an arrow.


The first number is the message number. This points to the message file. In a level's directory you'll probably find something like 04.txt (in addition to 04.nrm, 04.nrn, 04.npl, and 04.ol ...). This is the text for all the messages and where to find the sound file. In the Levels section of Lego.cfg, you point to this message file with NerpsMessageFile PATHTOFILE\filename.txt.

Here's the content of Driller Night's 01.txt:

\[You need four more Energy Crystals...\] #one#
\[You have stored two Energy Crystals now.\] #two#
\[Another Energy Crystal stored. You're more than half way there!\] #three#
\[Only one more Energy Crystal to go now...\] #four#
\[Congratulations! You've stored five Energy Crystals.\] #five#
\[Well done, you've found the tool store! Now collect five Energy Crystals...\] #six#

$one	sounds\Streamed\InGame\ins201
$two	sounds\Streamed\InGame\ins102
$three	sounds\Streamed\InGame\ins103
$four	sounds\Streamed\InGame\ins104
$five	sounds\Streamed\InGame\ins105
$six	sounds\Streamed\InGame\ins106

Their formatting is obscure, but follow it and you'll be good to go! I'm not sure what all those square brackets and slashes do. It seems like you can remove them and be fine in your own custom level. Yet these slashes and brackets never show up in the vanilla LRR levels.... hmm.



TRUE? SetMessage 2 1

will send message 2 - here in Driller Night that's "You have stored two energy crystals now." The 1 says "Don't display an arrow."

Chief will hang down from the screen providing he's not already onscreen from the level objective - this is why Frozen Frenzy's "Build up your base and make sure it's heavily defended" does not have Chief coming down, whereas Rocky Horror's "Build up base defenses and transport powerful mining vehicles" does. When he's finished his sound file and stopped speaking, he'll head back offscreen again. There seems to be no SetChiefFromRightSide or SetChiefGoAway command that I know of, and everything's handled automatically.

In this example, one thing that won't go away - the sound and Chief will - is the text at the bottom. For this we need to know about this next command:


TRUE? SetMessagePermit NUM

where NUM is either 0 or 1.

I've been quietly lying to you in saying that SetMessage 2 1 will do all these things. It's true that it will, but not off the bat in a script. This is because the default for messages is to block them and prevent them from coming through. You need to write

TRUE? SetMessagePermit 0

and then your messages will work. Oddly 0 => allow messages, 1 => disallow messages. This not only blocks Chief and the sound file, but it also blocks the text - or more accurately, clears the text.

(I don't know what happens if you send a message, block messages, and then reallow them again. Will you get the same text pop up? I don't think you do, and I haven't waded through this because I can't see an immediate use for it).


I'm also not sure if the default for messages is to block them. I always SetMessagePermit 0 just before I send a message anyway because MessagePermit might not be reset on level restart, stupid things might start happening, and if I set it to 0 then I know my messages should be coming through, so if they aren't and I'm trying to debug it I at least know that's not the issue.


You can’t seem to say concisely “Display message 1 and six seconds later go away.” Instead you have to say “Allow messages. Display message 1. Wait for six seconds. Then block messages.” (That's what I've found. There are some commands the DDI scripts use I can't get to work)...


If you take a look at the tutorial levels (which for a change are scripted sensibly) you’ll find a register is typically used as “where-am-I-when-it-comes-to-messaging.” This seems like a fairly effective use of a register, so I've used one here:

GetObjectiveShowing = 1 ? SetTimer1 0
GetObjectiveShowing = 1 ? Stop
; If objective is showing reset the timer and stop

; This is our "Where are we and what do we need to do?
GetR1 = 0 ? :WaitForFirst
GetR2 = 1 ? :ShutUpSixSecondsLater
GetR1 = 2 ? :SecondMessage
GetR1 = 3 ? :ShutUpSixSecondsLater
GetR1 = 4 ? Stop

GetTimer1 < 10000 ? Stop
; If ten seconds have NOT passed, stop.
; If they have, send our first message.
TRUE ? SetMessagePermit 0
; allow messages
TRUE ? SetMessage 1 1
; send message 1
TRUE ? AddR1 1
; change our counter of where we are
TRUE ? SetTimer1 0
; and start another timer which will come in handy
; that Stop is terribly important!

GetTimer1 < 6000 ? Stop
; Again, if six seconds haven’t passed, stop.
; But if they have…
TRUE ? SetMessagePermit 1
; block messages
TRUE ? AddR1 1
; increment our counter
TRUE ? SetTimer1 0
; reset the timer

GetTimer1 < 20000 ? Stop
; again, if twenty seconds have not yet passed, stop.
TRUE ? SetMessagePermit 0
TRUE ? SetMessage 2 1
; send message 2
TRUE ? AddR1 1
TRUE ? SetTimer 1 0
; hopefully that’s all explanatory!

I hope it's explanatory. I send a message, then block messages to clear the text six seconds later. Then I send another message (re-allowing messages again), then can call the same shut up function again. You would have to call a different one if you wanted the text to go away twenty seconds later, but if you want the same delay you may as well re-use your little label!


A nasty disadvantage of this script is that it only allows linear messaging. Say you want a message when you’ve build a Small Digger, and also a message when you’ve collected five Energy Crystals. If you were to use this script, there’s no neat way of allowing either message to happen first. You’d need separate variables to keep track of which one occurred – or alternatively you could use the fact that if one happened you know which one will happen next. It would be tricky to do that with a single variable, but doable - hence "no neat way."


Also, If you display a wall of text because your line in 01.txt was so long, the box will automatically jump up to make room for all the text (like the tutorial missions). Great stuff! Unfortunately it won’t jump back down to size again when you SetMessagePermit 1. It will jump back to size when Jet comes up, Energy Crystal in hand, or you can forcefully reset its size by creating a blank message that points to a silent file (I don’t know what happens if you point it to a file that doesn’t exist….) and then disabling messaging after that.




That’s…. really all there is to messaging that I know of. There are definitely more functions.


SetMessageTimerValues does something, but I have no clue what it does. AdvanceMessage presumably does something. GetMessageTimer probably has something to do with SetMessageTimerValues, but I can’t work out how to use it.
And more… but I can do all I want with just those functions I've outlined above. I do need a variable and a timer – perhaps those functions will allow me to not do this? Or perhaps those functions can do even more than what we see in DDI's levels?










Fun Things!


TL;DR Whatever you want, there's a function for it.


Basic Win/Loss Conditions

TL;DR GetCrystalsCurrentlyStored > Objective-1 ? SetLevelCompleted


The original scripts DDI made are extremely complicated. Take a look at Ice Spy, for instance. Loads of faff and the entire script could be condensed to 1 line as:

GetCrystalsCurrentlyStored > 24 ? SetLevelCompleted

Firstly, notice the two commands: GetCrystalsCurrentlyStored and SetLevelCompleted. Can you guess what they do? Correct! The first gets a number that is your crystals that are currently stored, and the second starts the victory teleport-out stuff (unless you said DisableEndTeleport TRUE in the Levels section of the cfg).

Secondly, Ice Spy has a victory when you have 25 crystals...That's not quite true. Suppose you were on 23 crystals and a Small Transport Truck unloaded 3 crystals into the Power Station. This would give you 26 crystals. If you had written

GetCrystalsCurrentlyStored = 25 ? SetLevelCompleted

then you never actually had exactly twenty-five crystals and so you wouldn't actually complete the level.


This is why a greater than 24 condition is used. On 23 crystals two upgraded STTs somehow manage to dump six crystals each into a Power Station at the same time, yielding 35 crystals in total? No problem, it's greater than 24, and so you win!



Curiously you don’t even need the oxygen counter for failure – if the oxygen level reaches 0 the game will quit anyway. Similarly you also don’t need a check for failure if tool stores and raiders - if you have absolutely nothing (including no power paths) the game will fail you of its own accord.



That is a very basic win condition and by far the bulk of vanilla levels. If you’re feeling imaginative you could have a requirement that it is ore you must collect. Or find the rock raiders in cavern X, or destroy all monsters, or…. well take a look at Don't Panic for NERPS scripting done well.


I don’t understand a lot of things here. I’m not sure on how the “bring object here” script works. But there are some commands that I know how to work very well. These are:

SetRockMonsterAtTutorial X
GetTutoBlockIsGround X

GenerateSlug needs nothing after it. Bam, one slug spawned. If there's no hole the game will be fine. If there's no hole and you've stuck it in a loop trying to create slugs and only exiting the loop when slugs are created the game will not be fine. It will freeze; it won't even crash nicely.

TRUE ? GenerateSlug

will work just fine. (And spam you if it's in the body of the script and not tucked away inside a label, as I’ve covered above)


SetRockMonsterAtTutorial requires a NUMBER after it that corresponds to the number in Tuto.map. It makes the default emerge monster (which is not always Rock Monsters! It’s specified in Lego.cfg in whatever level you have. You can even have EmergeCreature Slug and create slugs that way!) appear at all the blocks of that number in Tuto.map. This is unlike the Emrg.map, whereby running over a trigger only makes one monster even if there are loads of walls which are labelled as emerges.
This is by far the best way to make a horde of monsters attack the player. The other method is to have an EmergeTimeout of 0.0, which has its own problems – the horde never stops until the wall is drilled or reinforced. And if the trigger is in a spot where raiders will frequently run over.... yeah that's no fun to deal with.

So SetRockMonsterAtTutorial 1 would work. Or SetRockMonsterAtTutorial GetR1 . Or SetRockMonsterAtTutorial GetPoweredDocksOnLevel (no clue why you’d want this, but it’d work!) Don’t forget you need the condition!


GetTutoBlockIsGround also requires a NUMBER after it, like

GetTutoBlockIsGround 1 ? SetRockMonsterAtTutorial 2.

It’s quite explanatory: if Tutorial Block 1 is ground (as opposed to wall or ‘undiscovered cavern,’ then do something else. In this case, make Rock Monsters at tutorial block 2. Unfortunately, as you should notice, if you just leave this block lying in the middle of nowhere monsters will perpetually spam the player out of tutorial block 2. This is where labels and registers start coming into play!





Monster Invasions


Take a look at this…

GetTutoBlockIsGround 1 ? :CheckForInvasion


GetR1 = 0 ? :Invasion


TRUE ? SetRockMonsterAtTutorial 2
TRUE ? GenerateSlug
TRUE ? GenerateSlug
TRUE ? GenerateSlug
TRUE ? SetR1 1

When tutorial block 1 turns to ground – because I placed it in the middle of an undiscovered cavern – if R1 is 0 (which it will be at start!), make a bunch of monsters at tutorial block 2 and make three slugs. But then set R1 to 1. Then, when the NERPs scripter comes back again, tutoblock 1 is still ground and that will trigger. However, R1 is now 1 and so the invasion will not trigger again. Great stuff!


This is a useful way of making once-off invasions. There is a disadvantage that all the monsters will spawn at once, making it extremely convoluted and ‘look’ scripted, as opposed to a random monster invasion. While I could play around, I’m limited with my number of tutorial blocks and registers. (Actually, I might not be limited with tutorial blocks… Cyrem’s limit of 20 is solely self-imposed. I need to see if I can change this with a nice hex editor…) But we are limited for registers (or are we? see Future Research down below!)


Alternatively, the trigger could be GetCrystalsCurrentlyStored > 19 ? However, if the player’s drilled all the walls (or reinforced them all) before getting those crystals, the monsters won’t spawn, which is unfortunate.


As I’ve commented earlier in the Labels section, this needs some more faff in order for a whole bunch of invasions to run. An example you’re welcome to use is this:


GetR1 = 0 ? :Spot1OtherCheck
; Has the invasion NOT happened yet? Run the other check ---->
; now since the first invasion hasn't occurred, check for the next one!

GetR2 = 0 ? :Spot2OtherCheck

GetR3 = 0 ? :Spot3OtherCheck

GetR4 = 0 ? :Spot4OtherCheck

GetR5 = 0 ? :Spot5OtherCheck

GetR6 = 0 ? :Spot6OtherCheck

; highly neccessary!

GetTutorialBlockIsGround 1 ? :Spot1
; Is the cavern discovered that we want to summon the monsters in? If so, go ahead and make them --->
; but if that isn't, nothing happens
; and the next line tells it to go back up to Continue1
TRUE ? :Continue1

GetTutorialBlockIsGround 3 ? :Spot2
TRUE ? :Continue2

GetTutorialBlockIsGround 5 ? :Spot3
TRUE ? :Continue3

GetTutorialBlockIsGround 7 ? :Spot4
TRUE ? :Continue4

GetTutorialBlockIsGround 9 ? :Spot5
TRUE ? :Continue5

GetTutorialBlockIsGround 11 ? :Spot6
TRUE ? :Continue6


; Invasion 1 - when 1 is down, generate RMs at 2
TRUE ? SetRockMonsterAtTutorial 2
TRUE ? AddR1 1
; and make sure that the invasion 1 never triggers again

TRUE ? SetRockMonsterAtTutorial 4
TRUE ? SetR2 1

TRUE ? SetRockMonsterAtTutorial 6
TRUE ? SetR3 1

TRUE ? SetRockMonsterAtTutorial 8
TRUE ? SetR4 1

TRUE ? SetRockMonsterAtTutorial 10
TRUE ? SetR5 1

TRUE ? SetRockMonsterAtTutorial 12
TRUE ? SetR6 1

Unfortunately, you may notice that I’ve used up seven registers… one for each invasion. Still, registers are there to be used, and I don’t need them for anything else.






Slug Spam


TL;DR DDI screwed up their slugs scripts hard and you can do waaaaay better than that.


DDI’s slug scripts are awful. Atrociously bad. They will endlessly spawn slugs. If one dies, another one will rise to take its place. Back to Basics is an unneeded exercise in AI abuse.





My favourite slug script for simplicity is this.


GetMonstersOnLevel > GetSlugsOnLevel ? GenerateSlug


It makes the number of slugs equal to the number of (non-slug) monsters (including sleeping ones!). This ensures you never have to keep Action Stations on, that you will never be barraged with slug spam, and that killing the slugs (after you’ve killed the monsters) actually makes a difference.


For more complex scripts, a couple of commands become useful: GetRandom10 and GetRandom 100. DDI had another slug script hiding in their files which only ever shows up in-game in Rock Hard and it is basically this:

TRUE ? SetR1 0
; reset R1
GetRandom100 = 1 ? AddR1 1
GetSlugsOnLevel = 0 ? AddR1 1
GetR1 = 2 ? GenerateSlug

(There was also a requirement for more than 8 minifigures, but that’s easily met).
Play Rock Hard and notice how frequently the slugs come…. despite it being a 1 in 100 chance. The NERPS scripter runs waaaaay too fast.
If you wanted a 1 in 500 chance, that’d be a 2 in 1000, and you could write


GetRandom100 < 3 ? AddR1 1
GetRandom10 = 1 ? AddR1 1
GetSlugsOnLevel = 0 ? AddR1 1
GetR1 = 3 ? GenerateSlug

You could also remove the line about GetSlugsOnLevel (don’t forget to lower GetR1 = X or else it’ll never be reached!) if you wanted more than one slug.


As a much more detailed example, take a look at this...

TRUE ? SetR7 0
; resets R7
TRUE ? AddR7 GetRandom100
TRUE ? AddR7 GetRandom100
GetCrystalsCurrentlyStored < GetR7 ? :SkipSlugs
; I want this to happen more frequently later --> when crystal count is high
; so skip it when crystals are low
; Now re-use R7 as “how many slugs to spam” count.
TRUE ? SetR7 0
TRUE ? SetR7 GetRandom10
TRUE ? SetR7 GetRandom10

GetR7 < GetSlugsOnLevel ? :SkipMakingSlugs
; usual skip shenaningans
TRUE ? SubR7 1
TRUE ? GenerateSlug
TRUE ? :Sluggies
; This is a loop made the hard way…



; yeah that label use really isn’t efficient

Not only is there a random chance of slugs appearing, the random chance goes up with crystal count, and you get more than 1 slug at a time! Waaay better than Back to Basic's slug spam! All from constructing simple commands, some register shenanigans, and a health dose of labels.


There are a couple of notes that will prevent you from pasting this in any script you like.


Firstly, notice the loop. If there are no slug holes, and thus GetSlugsOnLevel will always be 0, the game will be stuck in that loop generating slugs to no avail. This will cause the game to freeze (the NERPs scripter seems to control advancing a frame... it's not just level completion!) But never fear, there’s a way around this. Put tutorial block 1 in all the caverns that have slug holes in them. Then say up the top of the script

GetTutorialBlockIsGround 1 = 0 ? :SkipAllSlugs

An odd use of GetTutorialBlockIsGround, but it does work this way. If none of 1 have been drilled, skip all slugs. As soon as 1 becomes Ground as opposed to Undiscovered Cavern, the slug script will now start.

(I don't know if GetTutorialBlockIsGround 1 = 2 ? :Stuff will work when two of tutorial block 1 have been drilled. Give it a whirl!)


A second note is that if your crystal count is about 200, then as you get very high the chance of GetRandom100 + GetRandom 100 being bigger than your crystal count is basically zilch. This means that you will get slugs really really really really really frequently. As in once every four frames. As in “lag-your-game” quantity of slugs.
There’s a simple way around this and that is the addition of more TRUE ? AddR7 GetRandom100 lines.  This does mean that slugs earlier are rarer.
But perhaps you could write something like…

TRUE ? AddR7 GetRandom100
GetCrystalsCurrentlyStored > 40 ? AddR7 GetRandom100
GetCrystalsCurrentlyStored > 80 ? AddR7 GetRandom100
GetCrystalsCurrentlyStored > 100 ? AddR7 GetRandom100


and then continue with the script as normal.

That would be interesting, but I’ve yet to test it like this. Give it a whirl!





For those not in the know, an RRU developer came over to the site and answered a lot of questions. It's still worth a read. Here are some things that particularly relate to NERPS scripting:


I do remember there being a [Please Talk Properly]-tonne of nerp functions, most of which were created especially for the tutorials (which, for a time, were the bane of my existance!!).



There are a few timers available, however 'Timer0' is reserved (It's used in 'nerpnrn.h'). It's best to just use 'Timer1' to begin with.

however this statement was perhaps nullified by


It may be possible to reclaim Timer0 if isn't actually being used anymore.



I'm guessing the script gets called once per game tick, with timers updated beforehand.

Yep, once per tick is correct

implying the script does in fact run 25 times a second



The slugs were still being added in while I was designing the levels, so they weren't really 'ready for use' until I was working on the later levels. I would have liked to go back and add them into earlier levels if time had permitted.

this explains why slugs only come in later despite being in those early cutscenes


and whilst we're on the subject of NERPS


It may surprise you to learn, but your compiler is actually better than what I had to work with originally. Our compiler had zero error checking, and it would simply crash if a syntax/spelling error was found (i.e. if it encountered an invalid symbol). To find errors in scripts I had to run the compiler through the debugger to see what line it was parsing when it crashed.

Your compiler, along with the couple of GUIs that have been created for it, are far far better than our original compiler, and that is no exaggeration.


and totally off topic but


If it makes you feel any better, my CFG editor was notepad ;P


Unfortunately we added debug keys with wild abandon for astonishingly useless purposes. For example, when I coded the oxygen requirement for the first time, I would have added in the O debug key to test the oxygen depletion condition. Once I was happy that it worked, I would never use it again. The same goes for most of they keys (e.g. finding the 'ideal' value for the 3D sound falloff, etc). All of these keys remained gathering dust for all eternity.


I actually got tasked with naming the levels too (apart from perhaps one or two - funnily enough Oresome wasn't mine

:P ). My favourite was always "Water lot of fun" for being so terrible.











TL;DR This is only the beginning! There's much more to be done!

I don’t know everything about NPL scripting. I’d like to think I know a lot, and I know a lot more than the standard GetCrystalsCurrentlyStored > 49 ? SetLevelCompleted . Other avenues I don’t understand:


- Bring Object To X . Take a look at Explosive Action’s script. As far as I understand, you tell the game to record the object on tutorial block such-and-such and then when it reaches tutorial block such-and-such you do things. However, according to unconfirmed any object present in the Object List file (it ends in .ol and there’s one for each level) will trigger the victory? Is there any way to say “If the Small Digger runs over this tile, do something, but if a Small Truck runs over this tile, don’t do something?” This is just future research.


- More messages. I’ve gotten them to work (huzzah!) but I don’t know how to use all the other functions. Do I even need them? Will they do ultra-special things? How do I even get them to work?


- Initial setup of building levels. As you will have noticed in Rocky Horror, you conveniently get given all the upgrades required to build a Support Station off the bat. (Why they give you a Geological Center is beyond me. It’d be loads more helpful to give me a Support Station!) Poke around those scripts and you’ll spot some useful commands, but there’s a lot that I don’t understand. What happens if you have two Support Stations and you try levelling up. What happens if the Support Station is Level 2 and you level it down? Can you specify levels in the object list?


- SetTutorialBlockIsGround X. As far as I understand, this destroys a wall – even Solid Rock – allowing you to send hitherto undetected erosion their way (good job alan! That map was Neuroticism Simulator 101), open a new cavern which you promptly flood them with monsters in, or simulate a landslide. However I have not yet gotten round to researching it. Experiment!


- Functions. Decompile a NPL with the provided decompiler and the output, though recognisably the same, looks nothing like the .nrm you usually write up. Does this have something to do with the curly braces {} that occasionally appear in the original LRR scripts? And what is the backslash doing in those scripts? A line break, or something more?


- Register usage. I like to use them as booleans. But they are integers. Surely I could store two boolean values by using two digits ; 11 (both on), 10 (one on one off), 01 (one off one on), 00 (both off?) Surely I could store up to fifteen booleans in a single register which goes up to 32000 if I bother to go through binary faff? Scripts for these would be complex but doable, and greatly extend our eight register limit if all we need are booleans!







Lastly, this guide would never have been possible without @Cyrem's fantastic NPL scripting tool, his fantastic Map Creator, and his fantastic forum he continually maintains.



EDIT1: fixed minor typos and formatting shenanigans

EDIT2: fixed some more typos (that was three slugs not five, something got lost in transit) and rewrote a nice section of Messages to be more logical and flowing

EDIT3: very minor changes



Edited by aidenpons

Share this post

Link to post
Share on other sites

Haven't got time to post a large reply right now, but, this is probably one of the greatest posts of recent. Good job putting this together, hope to see it expanded on the other areas.

Share this post

Link to post
Share on other sites
Slimy Slug

Very impressive work @aidenpons!  I have some of my own findings that I should probably post here as well to supplement the documentation you have.  I've been doing a lot of experimenting with the tutorial scripts specifically, which have been an absolute gold mine for researching purposes.


I'll gather my sources and post here at some point unless Cyrem beats me to it :P

Share this post

Link to post
Share on other sites

aaaaaaaaghfffffffblbe bah


Turns out that even STOP needs a CONDITION ? before it.


on its own does absolutely nothing.


TRUE ? Stop

does exactly what you need it to.

Share this post

Link to post
Share on other sites

I think I found the tuto block limit! Whilst working on one of my maps I noticed that command like SetTutorialBlockIsGround 60 were just flat out not working - but if I changed it to 34, they would work.


Cyrem's Map Creator has an arbitary limit of 20 but using Map Tool (another creation of Cyrem!) you can go up to 255. However... it looks like the tutorial block limit is 54.



Using a map set up like this (the Map Tool on the right is showing Tuto.map ; use the radar on the left to count), and a script that looks like

TRUE ? AddR1 1
TRUE ? SetTutorialBlockIsPath GetR1

GetR1 > 80 ? SetR1 0

which, for those that aren't familiar with NERPs, basically sends R1 running through 1 to 80 and sets the tuto block corresponding to R1 as a Power Path. So, in short, it should fill tuto blocks 1-80 through with Power Paths.





if I can count correctly, this means that the tutorial block limit is 54. Any higher and the game won't recognise it exists.


The next question is "Why?" That's the subject of more research. :P


(There is the possibility that tuto numbers bigger than 75 will start working again. Unlikely, but possible)




Share this post

Link to post
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

  • Recently Browsing   0 members

    No registered users viewing this page.

  • Create New...

Important Information

We have placed cookies on your device to help make this website better. You can adjust your cookie settings, otherwise we'll assume you're okay to continue.