Internal scripting

From Viki
Jump to navigation Jump to search
This page contains changes which are not marked for translation.
Other languages:
This article is a technical explanation about how and why the internal scripting exploits work under the hood. For a more practical explanation how to use internal scripting in your level, see Guide:Internal scripting. For a list of internal commands, see List of internal commands.

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 method only worked in VVVVVV 2.0.

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:

  1. 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.
  2. 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.

References