Internal scripting
The internal scripting exploit is an exploit within VVVVVV's scripting engine that allows for internal scripting commands to be used, thus giving the user more control than with regular simplified scripting. These commands are the actual direct commands in the game engine itself, and all simplified scripts are translated to internal commands under the hood. Internal scripting is a superset of simplified scripting in functionality.
There are two main ways to use internal commands: the say(-1) exploit and the load script exploit. The former produces cutscene bars while the latter does not, but the latter requires two scripts while the former does not.
Nowadays, most of the work normally needed to use these exploits can be automated by the internal scripting modes in Ved, but this article will detail exactly how and why they work.
say(6)/reply(6) method
This was the first exploit discovered, published by Stalefish in August 2011[1]. This method only worked in VVVVVV 2.0, because 2.1 increased the maximum number of lines in say
and reply
from 5, therefore it is obsolete today.
At the time, the main appeal of internal scripting was to allow text boxes to have colors other than cyan and gray, which was not yet possible in 2.0. Usage of it was discouraged because version 2.1 was going to both break this exploit as well as add colored text to simplified scripting.
Usage
This section will only explain how this method was used at the time - to create a colored text box.
1 say(6)
2 #
3 say(5)
4 #
5 text(color,0,0,lines)
6 A
7 B
8 C
Line 1 controls the sound, say(6)
for a terminal sound, reply(6)
for a player squeak.
Line 3 contols the position, say(5)
for a centered text box, reply(5)
for a text box above the player.
Lines 2 and 4 cannot be blank, so they're conventionally filled with #
characters.
Lines 5-8 can be used for internal scripting commands, along with one text box having 1-3 lines, using solely the text
command (no other of the usual text box construction commands).
It's likely possible to use a text
command on the last line with the right number of lines to eat automatically generated internal commands and prevent an unwanted text box from showing up, but this would not have much practical use as the newer methods are superior and work in more versions including 2.0.
Explanation
say(-1) method
This is the second exploit discovered, found by FIQ in 2012.
Usage
Usage of the exploit looks like the following. For clarity, gray lines are just the "glue", the rest are the actual internal commands you want to execute.
say(-1) text(1,0,0,4) say(10) text(gray,-500,-500,3) This is the first block of internal scripting! I'm going to play a sound effect. speak endtext delay(30) playef(9) delay(30) text(1,0,0,4) say(9) squeak(player) text(cyan,0,0,1) Wow! That was cool! position(player,above) speak endtext endcutscene untilbars loadscript(stop)
When run, this will play a terminal squeak (despite there being no squeak(terminal)
in the script), followed by a gray text box, then a sound effect, then Viridian's squeak with a cyan text box created above the player.
The basic format of a script using this exploit can be described as follows:
say(-1) text(1,0,0,4) say(N) <N-1 lines of internal commands> text(1,0,0,4) say(N) <N-1 lines of internal commands> text(1,0,0,4)
...
say(N) <N-1 lines of internal commands> text(1,0,0,4) say(N) <N-1 lines of internal commands> loadscript(stop)
The script starts with say(-1)
and text(1,0,0,4)
, followed by a series of say
blocks. The last line of each say
block is always text(1,0,0,4)
, except for the very last say
block, which ends with loadscript(stop)
.
This method will by default produce an extra squeak (depending on if you used the color argument of say(-1)
or not, or if you used reply(-1)
instead), and depending on the circumstances, it may be unwanted. This squeak can be removed by simply putting squeak(off)
at the top of the script. However, this removes an extra line of parser output per block (see Explanation below), so all text(1,0,0,4)
glue lines need to be replaced with text(1,0,0,3)
.
Any negative number of any magnitude can replace -1 in say(-1)
, but it has to be negative. All say()
s can be replaced with reply()
s. The optional color argument to say()
doesn't matter. The first three arguments of each text()
command (if it is a gray "glue" line) don't matter, only that the last argument is 4. However, by convention, the first argument is 1 and the second and third arguments are 0.
In 2.0, the maximum amount of lines you can have in a say()
command is 5. Starting from 2.1, this limit was raised to 50. When using this exploit (or the other one), you cannot have a text()
command and its associated speak
or speak_active
span over multiple blocks; both the text box and its speak
/speak_active
must be in the same block of internal scripting.
Explanation
There are two script parsers in VVVVVV - the custom script parser (scriptclass::load()
in 2.2 and below, scriptclass::loadcustom()
starting from 2.3) and the script runner (scriptclass::run()
). The former reads simplified scripts and translates them into internal commands, while the latter reads internal commands and executes them accordingly.
When the above script is fed through the custom parser, this is the result. For clarity, gray lines are again "glue" lines.
cutscene() untilbars() squeak(terminal) text(gray,0,114,-1) text(1,0,0,4) customposition(center) speak_active squeak(terminal) text(gray,0,114,10) text(gray,-500,-500,3) This is the first block of internal scripting! I'm going to play a sound effect. speak endtext delay(30) playef(9) delay(30) text(1,0,0,4) customposition(center) speak_active squeak(terminal) text(gray,0,114,9) squeak(player) text(cyan,0,0,1) Wow! That was cool! position(player,above) speak endtext endcutscene untilbars loadscript(stop) customposition(center) speak_active endcutscene() untilbars()
Let's compare the original script and parsed script, along with the reasons for why the parser wrote each line:
Original line | Parser output | Reason |
---|---|---|
cutscene() untilbars() |
An all-lowercase say() or reply() command was found in the script, or a say or reply command with capital letters was found but it had an argument separator.
| |
say(-1) |
squeak(terminal) text(gray,0,114,-1) |
This is the start of a text box, or at least the parser thinks it is. There were no squeak(off) commands beforehand, so a squeak gets added. Interestingly enough, the x and y being 0 and 114 is always hardcoded if you're using the gray text box.
|
text(1,0,0,4) |
(same) | The parser thinks this is the content of the text box. It only takes in 1 line, as -1 is not in the range 0..50 inclusive. |
customposition(center) speak_active |
This is the end of the text box. | |
say(10) |
squeak(terminal) text(gray,0,114,10) |
This is the start of another text box. No squeak(off) was found before this, so a squeak gets added again.
|
text(gray,-500,-500,3) This is the first block of internal scripting! I'm going to play a sound effect. speak endtext delay(30) playef(9) delay(30) text(1,0,0,4) |
(same) | These are the contents of the 10-line text box. |
customposition(center) speak_active |
This is the end of the 10-line text box. | |
say(9) |
squeak(terminal) text(gray,0,114,9) |
This is the start of another text box. No squeak(off) was found before this, so a squeak gets added again.
|
squeak(player) text(cyan,0,0,1) Wow! That was cool! position(player,above) speak endtext endcutscene untilbars loadscript(stop) |
(same) | This is the content of the 9-line text box. |
customposition(center) speak_active |
This is the end of the text box. | |
endcutscene() untilbars() |
We've reached the end of the script, and have inserted cutscene bars at the beginning of it. |
The main thing to take note here is the say(-1)
. When the custom parser encounters a say()
command, it will only read the number of lines given if it is between 0 and 50 (0 and 5 in 2.0). If the number given isn't in that range, then it will default to taking in 1 line.
However, that's not the end of it. The custom parser then translates the say(-1)
to text(gray,0,114,-1)
. When the script finally gets run, the text()
command has no such range restriction, so it will happily follow the order to take in -1 lines. This ends up being equivalent to taking in 0 lines, because its for-loop conditional ends up evaluating to 0 < -1
and never executes.
In this way, the internal command text(1,0,0,4)
is smuggled in and executed. This text()
ends up taking in the next four lines (customposition(center)
, speak_active
, squeak(terminal)
, and text(gray,0,114,10)
in as a text box, meaning they're essentially overwritten as they are no longer able to be executed. This then paves the way for the start of the internal script to be executed accordingly.
The same trick with text(1,0,0,4)
is used at the end of each block to override the intervening four lines, which in the case of the lines after the first block are customposition(center)
, speak_active
, squeak(terminal)
, and text(gray,0,114,9)
.
The last block uses loadscript(stop)
to avoid the extra speak_active
at the end of the script, as otherwise an extra unwanted text box would be created onscreen. loadscript(stop)
stops the script by loading a non-existent script; since the main game stop
script doesn't exist, the script runner sees there are no commands loaded, and stops running the script. However, it is also equally valid to use text(1,0,0,2)
instead, as that will skip over the customposition(center)
and speak_active
lines, and as a bonus, automatically do the endcutscene()
and untilbars()
without you having to supply them yourself. But it is, however, convention to use loadscript(stop)
.
In effect, the actual script executed in the end, with all the overwritten lines removed, is the following. The first three lines were not originally in the lines given in the custom script.
cutscene() untilbars() squeak(terminal) text(gray,-500,-500,3) This is the first block of internal scripting! I'm going to play a sound effect. speak endtext delay(30) playef(9) delay(30) squeak(player) text(cyan,0,0,1) Wow! That was cool! position(player,above) speak endtext endcutscene untilbars
Load script method
This is the third exploit discovered. It was accidentally found by FIQ in 2012, when he was messing around with scripting.
Usage
Explanation
Mixing internal and simplified
While it may seem like a script can only be either "internal" or "simplified", it is possible (but not recommended) to switch between simplified and internal scripting in a single script.
This is not recommended for a couple of reasons:
- All internal scripting is a superset of simplified scripting, therefore all simplified commands can trivially be translated to internal commands. Once you are in internal scripting, there is no gain from switching back to simplified.
- In terms of number of lines used, the custom script parser is a bit inefficient in some places with its translations of simplified to internal. If you wish to maintain compatibility with the pre-2.3 500 parser output script lines limit, then you would be better off sticking to internal scripting instead.
While you can transition from load script or say(-1) internal scripting to simplified scripting, you can only use the say(-1) method to jump back in to internal scripting.