to be continued~
get the rescue script here!
$LAYYYTER

titsay

Janaina Medeiros
he wasn't even looking at me and he found me

★
Not today Justin
cherry valley forever
wallacepolsom

Product Placement
we're not kids anymore.
Aqua Utopia|海の底で記憶を紡ぐ

blake kathryn

❣ Chile in a Photography ❣
2025 on Tumblr: Trends That Defined the Year

ellievsbear
art blog(derogatory)
Monterey Bay Aquarium

if i look back, i am lost
NASA
seen from United States
seen from United States
seen from United States

seen from Malaysia
seen from Serbia
seen from Russia

seen from Thailand
seen from Lithuania

seen from United States
seen from Malaysia
seen from United States
seen from Canada
seen from United States

seen from Mexico
seen from Peru

seen from Greece
seen from Türkiye

seen from Ecuador
seen from Russia

seen from United States
@lektoheart
to be continued~
get the rescue script here!

Anya is live and ready to show you everything. Watch her strip, dance, and perform exclusive shows just for you. Interact in real-time and make your fantasies come true.
Free to watch • No registration required • HD streaming
part one | part two this is a script i wrote for @lektoheart's rescue theater. i really recommend supporting development, and not just for sandbox mode; scripting things like this is easier than it looks and lots of fun.
you can get this (and two other scripts by me) here!
Shockjack
Here is a small minigame I made using the new rescue script features. I had the idea for this game after implementing structs. I thought it would be cool to test structs with a card game, since structs can be used to represent a card, a deck, a player's hand, etc.
The premise of the game is at you must gamble your health while playing rounds of blackjack. I thought about adding some sort of narrative to this. Like maybe a kidnapper is forcing their victim to gamble their life for a chance to escape. But I figured it might be better for the minigame to stand on its own. I also don't have a good environment to go with a darker scenario like that.
I think this game is an interesting example of a way to use rescue scripts for things beyond narrative experiences. If you want to see how this is done, the script will be posted on my discord (or if you want to play the minigame yourself). I will also post the full gameplay video on my discord.
Rescue Theater 0.8.2 Release
I just released a new version of Rescue Theater. Download it here.
I wasn't sure what to do for this release since I got very distracted with the new rescue script stuff. I wanted to put other stuff in this release, but I figured it would be better to put the rescue script stuff out now. That being said, I haven't gotten around to updating the rescue script documentation yet. I'm not even finished making tumblr posts about the new features.
I will post the script used in the choice panel demo onto my discord if you want an example of how to use some of the new features. I will also post an example of a more advanced script soon that is more of a minigame than a narrative story.
Change Log:
General
The game now freezes time when paused, when the choice panel is open, and when the application loses focus.
Made convulsion foam invisible while characters are hidden.
Rescue Script
Rewrote the rescue script system from scratch to implement many more scripting features. Read my posts on tumblr for more information.
Added shorthand dialogue format to Rescue Script. dialogue.label(R).text("Hello World"); -> R "Hello World";
Added shorthand wait format to Rescue Script. script.wait(0.5); -> wait 0.5;
Added async blocks.
Added 'camera.setCustom' command.
Added copy-paste pop up for setting custom camera.
Added 'camera.path' command.
Added choice panel.
Changed script advance button to a 2x speed multiplier toggle. This makes testing easier.
Added tags for [v:girlfriend], [v:woman], [v:female], [v:wife], and their male equivalents.
Scripts now auto-reload when going to the menu and restarting the game. This means they don't have to be selected again every time they're edited.
Added 'heart.showMonitor' and 'heart.hideMonitor' commands.
Added 'heart.sound' command. This accepts the argument of 'NONE', 'HB', or 'BEEP'. Note that this doesn't overwrite the user's preferences. The change is only temporary and will reset when the scene is restarted.
The dialogue box's text will now resize to fit the box better if there is no label.
Structs in Rescue Script
In my last post I mentioned that I might add structs to the language. As soon as I thought of the idea, I couldn't stop thinking about how that could be done. I have a bad habit of getting distracted by things like this. Anyway, implementing structs turned out to be easier than I thought it would be, so I added them in.
What are structs? The name structs stands for structures, and they are named that because they are a way to structure a group of data. It's kind of a weird name and I considered naming them something else in rescue script. Some languages call them records. But I figured I would keep the struct name since it's what I'm used to. Also, calling them structs will have less naming conflicts because who was gonna name a variable or function 'struct'?
An example of a struct looks like:
A struct definition starts with the 'struct' keyword followed by the struct's name. The body of a struct is just a comma separated list of identifiers. Within a struct, these identifiers are called members. The struct definition only contains the list of its members, it can't give any of these members default values.
To create an instance of a struct, you must call the name of struct definition as if it were a function. This call takes 1 argument for each of the members, and it will initialize these members in the same order that they are defined within the struct. When creating a struct instance, you must initialize each of its members. I implemented structs this way because I like that it encourages procedural programming patterns. If you want to have default values, you just need to create your own constructor function:
This snippet shows how you can construct the 'Character' struct with only a 'name' and an 'age'. The 'favoriteFood' and 'health' members are given default values.
The members of a struct instance can be accessed using a '.' followed by the member's name.
This snippet shows how the members can be retrieved and assigned to, similar to the way variables are used.
Practical Example
To show how structs can be used within the context of a rescue script, let's go back to the randomization example. In the last post I showed how to randomize lines of dialogue. But what if we wanted the rescue mode to behave differently depending on the dialogue? For that, we would need a way to group related data. This could be accomplished purely with arrays like this:
Here we need one array for each data type, and an additional array for the indices of the other arrays. This additional array is required for using the better randomization algorithm granted by the 'random' native function. If we didn't care about that, we could get a random index like: 'let randomIndex = random(0, length(shockLines));'. Either way, this solution is a bit messy and hard to read. It's difficult to tell which piece of data goes with what, and if we wanted to add another entry we would need to modifiy 5 different arrays. With structs, this is much nicer.
With structs we only need one array. The data within this array is neatly grouped, making it easier to read and easier to add more entries. After retrieving the random 'ShockEntry' instance, accessing its members is also more readable rather than indexing into arrays.
I will go into more examples in the devlog section, but I think this is enough to understand how useful structs can be.
Devlog
Implementing the structs was a lot easier than I thought it would be. Behind the scenes, a struct is nothing more than an array of data. The only difficult thing here is mapping the names of the struct's members to the indices of the array.
There was a couple of options to handle this. Ideally, these mapping would be resolved at compile time. That way accessing a struct's member at runtime would have the best possible performance. Accomplishing this would be difficult though, as you would need to track which variable was which struct. And since rescue script is a dynamic language, a single variable could contain multiple types of structs throughout the execution of the script.
In reality, these names need to be mapped to the internal array at runtime. The question is, which algorithm offers the best performance? The obvious choice is a hash map. After all, hash map's are constant time. Unfortunately, that constant time is pretty slow and hash maps also have bad cache locality. Hash maps are only worth it when you're searching for more than 25 items. While structs can contain up to 255 members, most will have less than 10.
Even though the mappings can't be resolved at compile time, at least we have the full list of members ahead of time. That means the list can be presorted. This is the perfect case for a binary search. Except it turns out that binary search is also pretty slow, only becoming useful once the list has more than 128 items. This surprised me, as on paper the algorithm should be fast even with few items. The reason its slow comes down to low-level hardware architecture with things like cache locality and branch prediction.
Sometimes the simplest solution is the best one. In this case, I just used a linear search. Linear searches are considered slow because they need to check the string character by character. The benefit here is that the equality check can bail out early as soon as it sees a character mismatch. And the equality check can be skipped entirely if the length of the strings mismatch.
The point here is that resolving the member names at runtime isn't ideal, but in practice its not that bad.
Object Oriented Principles
I mentioned in my previous post that I wouldn't make rescue script an object oriented language. The reasoning is that it would be way overkill for a simple scripting language. I also think the syntax would clash with the existing commands and I'll make a whole post talking about those soon.
That being said, structs do allow some object oriented principles to be emulated.
The idea of OOP is that it allows for objects to contain data and behavior. Structs can contain data (its members), but not behavior. However, in rescue scripts we can have functions that operate on structs. Here's an upgraded example of the resizable list from my last post.
Instead of operating on global variables, the 'ListAdd' function operates on a 'List' struct instance. This allows for multiple instances of lists to exist at once, compared to the single global list. Another change is the new 'arrayCopy' native function. I added this as a faster alternative to iterating manually through the arrays and copying data. According to my benchmarks, this new native function is 25% faster.
Using the list looks like:
Structs allow for all sorts of data structures to be implemented. Note that a struct can contain instances of other structs in its members. It can even contain instances of itself. That allows for things like linked lists and trees.
Inheritance
In C, you can accomplish inheritance by having children structs contain their parent struct as their first member. In rescue script, that would look something like this:
In this case, the 'base' member would contain an instance of 'Point'. Since rescue script is a dynamically typed language, nothing is actually enforcing this though. And there's also no need for casting. The 'PrintPosition' function doesn't really care what 'base' is, as long as it contains a member named 'x' and 'y'.
A potential downside of this technique is that the parent 'Point' struct wouldn't work with the 'PrintPosition' function. Here, 'Point' acts as more of an abstract base class that shouldn't have concrete instances. Going back to my previous point, we don't really need the concept of the 'base' member. We would likely be better off doing something like this:
Again, the 'PrintPosition' function doesn't really know what 'self' is. It could be a 'Point', a 'Coord', a '_3DPoint'. The only thing that matters is that it has a member named 'x' and 'y'. As someone who has mostly used statically typed languages, this behavior feels really strange to me.
Polymorphism
Polymorphism can also be emulated with structs. Remember from my post on value types that functions are a value. That means the members of a struct can be set to a function, and that function can then be called using that member's name.
In this example, each instance of the 'Animal' struct behaves differently when the 'MakeSound' member is called.
The big OOP principle that rescue script is missing is encapsulation. There is no way to limit access to members or functions. Everything is available everywhere.
Is any of this stuff actually useful for writing rescue scripts? Maybe. I'm mainly highlighting it here because I myself am surprised at how many things a simple feature can accomplish. After all, a struct is nothing more than an array with named indices. As someone who has mostly used static-typed object oriented languages, playing around with a dynamic typed procedural language has been pretty fun.

Anya is live and ready to show you everything. Watch her strip, dance, and perform exclusive shows just for you. Interact in real-time and make your fantasies come true.
Free to watch • No registration required • HD streaming
Arrays in Rescue Script
I wasn't originally planning on adding arrays to rescue script. But after working on the choice panel demo, I realized there was a good use case for them. In the demo, there is a section where the user can decide how many times to use the aed shock. During this section, I added some randomized dialogue. To randomize the dialogue, I created this function:
This function does work, and it's a good example of the typical way to randomize things. The problem with the above example is that it's not very reusable. If I wanted to add another possible dialogue line I would need to add a new if statement and then change all the probabilities in every statement. And it would be annoying to have to write a custom function every time I wanted to pick a random option from a list of options.
Since this type of randomization is very useful for narrative scripts, I wanted to add an easier way to accomplish it. And that's where arrays come in. In rescue script, an array is nothing more than a list of values.
let x = array(5);
The above snippet shows how to create an array. The only way to create an array in rescue script is to use the 'array' native function. This function is overloaded and can take a dynamic number of arguments. If it's passed a single numeric argument, the 'array' function will return an array with the argument's length. The above example creates an array of 5 elements with each element being defaulted to 'null'. Note that the given length is fixed, meaning that arrays can not grow or shrink.
let x = array(5); x[0] = "Hello"; x[1] = "World"; x[2] = 5; x[3] = true; x[4] = false;
The above snippet shows how to set the elements of an array. The square brackets are the syntax for indexing into an array. Note that the index starts at 0 and goes to the array's length - 1. This may be confusing to non-programmers, but it is the standard way of indexing things so I didn't want to change that.
for let i = 0; i < length(x); i = i + 1 { print x[i]; }
Retrieving an element from an array also using the same square brackets and indexing. I mentioned earlier that the only reason I added for loops was for iterating through arrays. The above example shows the standard way to do that. It also shows another native function: 'length'. The 'length' native function returns an array's length. It can also return the number of characters in a string if it's passed one. I don't really know how that could be used, but its the only other value type that has a length component so I added that in.
Another way to initialize an array is to pass in the values to the 'array' native function.
let x = array(1, 2, 3, 4, 5);
When the 'array' native function is given multiple arguments, it will return an array containing those elements. It will also calculate the proper length needed to hold the given elements.
So, how does this help with randomization? For this I added an additional overload to the 'random' native function. If passed an array as an argument, the function will return a random element from that array. With that, I could replace the above example function with:
The above snippet is so much easier to work with. Not to mention, I had another reason for adding this.
Randomization Kind of Sucks
When filming the choice panel demo, I actually got pretty lucky in that I didn't get too many repeated dialogue lines. Usually when I play through the demo's script I get a lot of repeats. It feels pretty bad to have something be randomized but get the same result many times in a row. Now that I've got the 'random' function working with arrays, I could implement a more complex algorithm rather than relying on true randomness. So here are some options for a better random algorithm.
The Shuffle Method
Instead of grabbing a completely random index, the order of the elements in the array can be randomized instead. Whenever a random value is requested it grabs the last element in the array and removes it from the array. When the array is empty, it is repopulated with the original values, and the orders of those values are randomized again. This ensures that an element of the array can't be seen again until every other element in the array has been seen.
This method works and is very popular, but I don't like it for two reasons:
Patterns emerge within the shuffles. For example, lets say we have an array with six elements: [1, 2, 3, 4, 5, 6]. Two shuffles of the array might look like:
[2, 4, 5, 3, 6, 1] [1, 3, 6, 4, 2, 5]
The whole point here was to eliminate repeats. And yet, we have a repeated '1'. The last value in the first shuffle is the same as the first value in the second shuffle. Of course, we could check for this scenario and reshuffle if it occurs. But that doesn't completely solve the problem.
[2, 4, 5, 3, 6, 1] [6, 1, 3, 4, 2, 5]
This eliminates any direct repeats, but there is still a '1' very close to another '1', as well as two '6's near each other. Shuffles are great for getting an even distribution within the shuffle, but not great at getting an even distribution at the borders of shuffles.
Outside of the previous issue, I feel like shuffles kind of ruin the point of randomization. For example, if you know the array has six elements and you see a '1' then you know that you won't see a second '1' for at least five more values. The fact that shuffles have a perfectly even distribution (outside of the borders) is in itself a pattern. This pattern is pretty subtle, but I personally don't like it.
History Checking
This second algorithm works on a very simple idea. Let's start with the most naive approach to solving this pattern problem. To solve the repeats, all we have to do is keep track of the last retrieved index. Then if that index is randomly chosen, we will choose a different random index instead.
1, 2, 1, 2, 1, 2
That solves the problem of direct repeats, but we still have the problem of obvious patterns. The trick here is that we don't just want to check the last found index. We want to check the last found N indices were N is half the length of the array. So for our example array with 6 elements, we would keep a buffer of the last 3 found indices. With that algorithm, we could get a result like:
2, 4, 5, 3, 2, 6, 5, 4, 3, 1, 6, 2
The result has a fairly even distribution, but it doesn't have every number appear exactly once before it can appear again. It also has no issues with borders. This algorithm also scales well with the length of the array. This example uses an array with length 6, which means the history buffer is 3. It would work even better if the array had 10 values and a history buffer of 5.
True Random
I already used this history checking algorithm for choosing random sound effects in the game. So implementing this within the 'random' native function was very easy. So if you want a better random value, be sure to make an array and use the 'random' function. If you actually need true random, you can easily accomplish that with:
let x = array(1, 2, 3, 4, 5, 6); let trueRandom = x[random(0, length(x))];
This will choose a random index between 0 and the length of the array using a true random value. (actually, computers can't generate truly random numbers but that's beyond the point.)
Anyway, this post on arrays was mostly about randomization but arrays can be used anywhere a list of values would be helpful. For example, maybe you need a function that returns more than one value. Then the function could store the values in an array and return that. Or it could be helpful for organization to have one array rather than a bunch of related global variables.
The main downside of arrays is that they have a fixed size. I considered making them resizable, but that makes it a lot harder to implement the history checking algorithm. If you do need resizable arrays, you can always implement them yourself in rescue script. Here is a quick example I came up with:
This example is one of the rare cases where rescue script could benefit from being object oriented. That's not going to happen though. Though maybe at some point I'll implement structs.
Functions in Rescue Script
Functions are another form of flow control. The syntax for defining a function looks like:
function example() { // Function Body }
The definition starts with the 'function' keyword. Following that is the function name, which can be any valid identifier. Then there must be an opening and closing parenthesis, I'll explain why later. On its own a function won't do anything. In fact, during execution the VM will just skip right past it. To execute the code within the function's body, you must use a function call.
example();
The syntax for a function call is very simple. It's just the function's name followed by an open and closed parenthesis. The real benefit of functions is that they allow the user to define a block of code once, then call it multiple times elsewhere. Functions are great for organization because they allow for less duplicated code. Also, if you need to change the code within a function you only need to change it once rather than searching for all the repeated code throughout the script.
This functionality alone is great, but functions have a few more features. The next thing to know is that a function can take arguments.
function example(x, y) { print x + y; }
In the above snippet, 'x' and 'y' are arguments to the 'example' function. Functions can take from 0 to 255 arguments. During a function call these arguments become local variables belonging to the function's scope. The difference between using arguments and defining local variables manually is that arguments need to be passed in during the function call.
example("Hello ", "World"); // prints "Hello World" example(5, 10); // prints 15
Using arguments allows the function to behave differently depending on the values passed in during the function call. Functions have one more important feature: return values.
function add(x, y) { return x + y; }
Outside of just executing the code within the function body, a function can also return a value using the 'return' keyword. This works because function calls are expressions, meaning they resolve to a value.
let x = add(5, 10); print x; // prints 15
Note that all function calls will return something, even if the function doesn't have a return statement. This is because a function will implicitly return 'null' if there isn't an explicit return statement. It's also important to note that a return statement will immediately end the function and go back to the function call.
function loop() { let x = 0; while true { if (x >= 5) { return; } x = x + 1; } }
The above snippet contains a while loop that appears to be infinite. However, the return statement allows the loop to be broken when x is greater than or equal to '5'. Also, a return statement without a value will return 'null'.
Another thing about functions is that they can call themselves. A function that calls itself is a recursive function. You need to be careful when working with recursive functions as they can easily create an infinite loop.
function recursive(x) { if (x >= 5) { return; } recursive(x + 1); print x; } // prints '4', then '3', '2', '1', and '0'. recursive(0);
Native Functions
Rescue script contains a few native functions. These are built in functions that can be called like user-defined functions. The difference is that native functions run on the native C# directly, so they are a bit more efficient. So far, the following native functions are available:
Random
'random' is the most important native function to understand for rescue script writers. Other than the choice panel, the 'random' native function is the only way to add dynamic variation to the flow of a rescue script. This is because the 'random' function will return random values, which means the script can have different outcomes every time it's run.
The 'random' function is overloaded. An overloaded function is one that behaves differently depending on the number of arguments it's given. Rescue script does not support user-defined overloaded functions. Only native functions can be overloaded.
print random(); // In my case, this printed: '0.3909457'
When passed 0 arguments in the function call, 'random' will return a random decimal number between 0 and 1.
print random(10, 20); // In my case, this printed: '14.58026'
When passed 2 arguments in the function call, 'random' will return a random decimal number between the values of the given arguments. Note that the 2 arguments must be numbers.
Since 'random' returns decimal numbers, what if you need a random whole number? For example, the camera.fixed command only takes whole numbers.
camera.fixed(random(1, 3));
As mentioned in my post on rescue script value types, the script will convert a decimal number to a whole number by cutting off the decimal place. This means the above snippet will work fine for randomly choosing fixed camera 1 and 2, but it will never pick camera 3. This is because the 'random' function excludes the endpoints in its calculation. The random value may be '2.99999999', but it will never be exactly '3'. And '2.999999' will resolve to '2' when the decimal portion is chopped off.
If you want to get the whole numbers between 1 and 3, you could just do:
camera.fixed(random(1, 4));
This works fine for commands that take whole numbers. But what if you want to convert to a whole number within the rescue script itself. For that, I added another native function.
Round
The 'round' native function takes 1 number argument and it will return that number rounded to the closest whole number.
print round(1.2); // prints 1 print round(1.8); // prints 2
This one is pretty self explanatory. The only interesting thing here is that it uses banker's rounding. This means if the decimal portion is exactly '0.5' it will round to the nearest even number. This creates a more even distribution when rounding many numbers rather than always rounding up. I don't know how useful this is, but it was the default way C# rounds things so I just kept it in.
print round(1.5); // prints 2 print round(2.5); // prints 2
Clock
This native function probably isn't too useful for most rescue script writers. I added this one to help benchmark the scripting system. This function takes 0 arguments and it returns the number of seconds since a script has started.
wait 2; print clock(); // prints '2.012187'
I have implemented a few more native functions for dealing with arrays, but I will discuss those during my post on arrays.
Flow Control in Rescue Script
Flow control is the mechanism that allows a script to decide which parts of the code to execute. In the old version of rescue script, there was no flow control. The script would just execute each command one after the other until it reached the end. Now the flow of a script can be more dynamic.
I've already shown one method of flow control: the choice panel. The choice panel is the driver of all the other flow control mechanisms because it allows the user to change the flow of the script at runtime. The reason I didn't implement any flow control in the old version of rescue script is because it doesn't matter how much the program jumps around if the end result would always be the same. But the choice panel (along with the 'random' native function, which will be discussed later) is what allows the script to have different outcomes every time it's run.
If Statements
An if statement is the classic flow control mechanism, and it looks like:
if condition { // Block A }
The if statement starts with the 'if' keyword followed by a condition. A condition is any expression that results in a boolean value (true or false). If you need a refresher on boolean expressions, be sure to read my post on rescue script value types. If the condition resolves to 'true', the code in "Block A" will execute, otherwise it will be skipped over.
In many languages, the condition must be surrounded by parenthesis. In rescue script, I omitted the parenthesis so the syntax is more consistent with the 'choice' and 'option' blocks. This means that the body of the if statement must contain the curly brackets. One liner if statements are still possible, you just have to include the curly brackets like so:
if condition { print "Hello World"; }
If statements can optionally contain an else block.
if condition { // Block A } else { // Block B }
In this case, if the condition is 'true' the code in "Block A" will execute, otherwise the code in "Block B" will execute. Note that else blocks also require the curly brackets. This does mean else-if statements aren't possible, although I might look into fixing that at some point.
And in case you want to see what the condition might look like, here is a practical example.
let useAED = false; if useAED { aed.shock(250); } else { defib.shock(250); }
While Loops
If statements are great for running one block of code or another. But what if you want to run the same block of code multiple times? For that, you need a while loop:
while condition { // Block }
A while loop will repeat the code within its block as long as the condition is 'true'. If the condition isn't 'true' in the first place, the code within the block won't even run once. Because the condition is checked every time, the condition expression must be modified within the loop otherwise it will loop forever.
A practical example of a while loop is:
let i = 0; while i < 5 { print i; i = i + 1; //Sadly, i++ isn't implemented }
The above snippet will print out "0", then "1", "2", "3", and "4" before the condition becomes 'false' and the loop exits. If the condition never becomes 'false' the loop will continue forever, which is often a mistake. I will talk more about infinite loops later in this post. Another way to escape an infinite loop is with a return statement, but I will discuss more about those on my post about functions.
For Loops
I wasn't originally planning on adding for loops. After all, anything that can be done with a for loop could also be done with a while loop. However, after writing the choice panel demo I realized there was a good use case for adding arrays to rescue script. I will make a separate post about arrays, but for loops are really nice for iterating through arrays. So I decided to add for loops as well.
for initializer; condition; incrementer { // Block }
For loops are defined with 3 optional components: the initializer, the condition, and the incrementer. These components are separated by semicolons, except the incrementer which is ended by the required curly brackets.
The initializer is typically a variable declaration. This variable will be part of the for loop's scope, making it a local variable. Technically this part can take any expression, so it could set the value of an existing variable instead of declaring a new one. The important thing to note here is that the initializer runs only once before the loop starts.
The condition works the same as in the while loop.
The incrementer is used to change the value in the condition so that the loop can eventually end. However, it will technically take any expression. The incrementer is a bit odd as this expression will execute at the end of the loop before starting the next cycle even though it is defined at the beginning of the loop. Here's a practical example of a for loop:
for let i = 0; i < 5; i = i + 1 { print i; }
Note that this snippet accomplishes the exact same thing as the while loop example above. The benefit here is that it moves all the boilerplate code from within and around the while loop into the declaration of the for loop.
Infinite Loops
The introduction of loops is great, but a simple mistake could lead to the dreaded infinite loop. Infinite loops are a big problem for unity games because the main game loop in unity is single-threaded. This means that if a user creates an infinite loop it will freeze the program indefinitely. Then the user will have to open their task manager and manually end the task. That's not a great user experience.
Because of this, I decided that the virtual machine that executes rescue scripts should run on a separate thread. This made developing the system a lot harder, as cross-thread communication is difficult. I will save the discussion on some of those difficulties for future devlogs. The point here is that an infinite loop will not freeze the game. If you accidentally create an infinite loop, you can just cancel the script using 'F3' or go back to the main menu and restart the game.
That being said, maybe you want to make an infinite loop on purpose. There isn't much of a reason for doing this now, but maybe in the future I will add more features that could make infinite loops desirable. If you do want to make an infinite loop, there are a few things to keep in mind.
while true { wait 0; }
The first thing to note is that infinite loops should contain at least 1 instruction that halts the VM. I'll explain more on what halting means in a future post, but you can think of them as any command that takes time to complete. This is because if the VM's thread never sleeps it will endlessly loop in a pattern that's called "spin-waiting" or "busy-waiting", which is a bad practice since it wastes cpu resources. In the above example, waiting for 0 seconds actually waits for 1 frame due to the way command dispatch works. So the above loop will execute once per frame.
Another thing that I recently learned is that the above snippet isn't the optimal way to write an infinite loop. The problem with the above while loop is that it will check the condition every time it loops. This is wasteful since 'true' is always going to equal 'true'. The optimal way to write an infinite loop is actually with a for loop.
for ;; { wait 0; }
As mentioned above, all the parts of a for loop are completely optional. This means you can leave out the initializer, the condition, and the incrementer. The above loop will never check a condition since there's no condition to check. When you look at the resulting bytecode, the above snippet is a completely pure loop. At the end of the day this is definitely a micro-optimization, but if you want to be a nerd this is the optimal infinite loop.
Devlog
Implementing flow control in the VM is actually pretty easy. The VM stores something called the instruction pointer (ip for short). The ip points at the current bytecode operation to perform. In normal operation, the VM will perform the operation, then increment the ip to point at the next opcode. The great thing here is that the instructions don't have to be executed in order. The ip can be set to an arbitrary value at any time, and the VM doesn't care. It will continue operating as if that was always the next opcode to execute.
In practice, changing the ip is done with jump instructions. Rescue script has a few jump opcode. OP_JUMP_AHEAD and OP_JUMP_BACK will move the ip forward or backward by the given amount. OP_JUMP_IF_FALSE will first look at the top value on the stack. If the top value is false, it will jump forward, otherwise the VM will continue as normal.
With these opcodes in mind, here is an example if statement:
if true { print 5; } else { print 10; }
And the resulting bytecode
0000 1 OP_TRUE 0001 2 OP_JUMP_IF_FALSE 1 -> 11 0004 | OP_POP 0005 3 OP_IMMEDIATE 5 0007 | OP_PRINT 0008 4 OP_JUMP_AHEAD 8 -> 15 0011 | OP_POP 0012 7 OP_IMMEDIATE 10 0014 | OP_PRINT 0015 8 OP_NULL 0016 | OP_RETURN
If you follow the arrows on the right to the instruction number on the left, you can follow along for both the true and false paths of the if statement's condition. While loops are even simpler.
while x < 5 { print x; }
0004 2 OP_GET_GLOBAL 0 'x' 0006 | OP_IMMEDIATE 5 0008 | OP_LESS 0009 3 OP_JUMP_IF_FALSE 9 -> 19 0012 | OP_POP 0013 4 OP_GET_GLOBAL 0 'x' 0015 | OP_PRINT 0016 5 OP_JUMP_BACK 16 -> 4 0019 | OP_POP 0020 | OP_NULL 0021 | OP_RETURN
One of the challenges when writing the compiler is knowing where to jump to. This is because the rescue script compiler is a single-pass compiler. That means it needs to write out the bytecode as each token is parsed with no knowledge of future tokens. Jumping backwards is easy, as the starting point of the loop can be stored until the end is reached. But how can you jump forward if you can't see into the future and know where the block statement will end?
Accomplishing this uses a cool technique called back-patching. How it works is that the compiler actually writes a dummy jump value when it needs to jump forward. For example, OP_JUMP_AHEAD 0x00 0x00 (Note that the jump offset uses a 2 bytes as a single byte would only allow a jump of 256 instructions). The position of this dummy value in the bytecode is saved for later until the end of the block statement is reached. Once the compiler discovers where it needs to jump to, it can go back to this position in the bytecode and patch the dummy value with the real jump position.
Variables in Rescue Script
Variables are how a script stores a value. In rescue script, defining a variable is very simple.
let x = 5;
Defining a variable starts with the 'let' keyword. Then comes the variable's name. After that, an initial value can be assigned, although that is optional. If there is no initial value assigned, the variable will be assigned the value 'null'.
It's important to note that rescue script is a dynamically typed language. That means that variables don't contain a specific type. In the above example, x is assigned the value '5'. But the following snippet is perfectly valid.
let x = 5; // Here x is a number x = false; // Now x is a boolean x = "Hello"; // Now x is a string
I personally prefer typed languages, however I made rescue script dynamically typed for a couple reasons. The first is that it was easier to implement in the compiler and the VM. The second is that I think dynamically typed languages are easier to use for non-programmers.
While dynamically typed languages are easier to use, they are also easier to make mistakes with. If the variable is accidentally set to the wrong type it could result in a runtime error. So that is something to be careful with.
To get the value of a variable, all you need to do is write the variables name.
let x = 5; print x; // Prints '5'
To set the variable to a new value, use the '=' operator.
let x = 5; x = 10; print x; // Prints '10'
Global Variables
To understand the difference between global and local variables, I first must explain scopes. A scope is defined by any code surrounded by curly brackets. Scopes are usually attached to things like if-statements, while-loops, and functions. However, scopes can also be defined on their own.
let x = 5; { let y = 10; }
In the above snippet, the 'y' variable is declared in its own scope. Global variables are variables that are declared outside of any scope. In the above example, 'x' would be a global variable. Global variables are variables that can be accessed anywhere in the program. However, there is one caveat. A global variable must be declared before it is used. More specifically, it must be declared before the script executes the code that uses the variable, which isn't necessarily earlier in the script. For example:
function test() { print global; } let global = "Hello World"; test(); // test function is executed here
The above snippet is fine, even though 'global' is defined later in the script than the function that uses it. That is because the function isn't executed until after the global variable is declared.
Even though the above example is valid, its probably best to write all the global variables at the top of the script. This is usually better for organization, rather than having variable declarations scattered throughout the script. Another important thing to note is that you can't declare two global variables with the same name. This will return an error.
Local Variables
Any variable that is declared within a scope will be a local variable. Local variables are different from global variables in that they can only be accessed from within the same scope that they are declared.
{ let y = 10; } print y;
The above snippet is invalid, as the scope that contains the variable 'y' closes before the print command.
Another feature of local variables is that they can "shadow" global variables. This means that a local variable with the same name as a global variable (or any variable in an enclosing scope) will be prioritized over the global variable.
let x = 5; { let x = 10; print x; // This prints '10' } print x; // This prints '5'
And here is an example of a local variable shadowing another local variable
{ let x = 10; { let x = 15; print x; // Prints '15' } print x; // Prints '10' }
The concept of shadowing may seem tricky at first, but it can be very useful.
Devlog
There is another difference between global and local variables, and that is how they work behind the scenes. Global variables are stored in a dictionary, which is a data structure that allows values to be looked up by a key. In this case, the key is the variable name. Retrieving a value from a dictionary is pretty fast. However, finding the value of a local variable is even faster.
This is because local variables are actually stored on the stack. Declaring a local variable is as simple as pushing a variable onto the stack. The program remembers the index in the stack of the local variable, and this actually happens at compile time. So at runtime, the script doesn't know the names of the local variables at all, only their position on the stack. If you look at the following example, x is stored at index 0 on the stack and y is stored at index 1. After the scope ends, the two local variables are popped from the stack.
{ let x = 5; let y = 10; x = 15; y = 20; print x + y; // Outputs '35' }
Here is the bytecode for the above snippet:
0000 OP_IMMEDIATE 5 [ 5 ] 0002 OP_IMMEDIATE 10 [ 5 ][ 10 ] 0004 OP_IMMEDIATE 15 [ 5 ][ 10 ][ 15 ] 0006 OP_SET_LOCAL 0 [ 15 ][ 10 ][ 15 ] 0008 OP_POP [ 15 ][ 10 ] 0009 OP_IMMEDIATE 20 [ 15 ][ 10 ][ 20 ] 0011 OP_SET_LOCAL 1 [ 15 ][ 20 ][ 20 ] 0013 OP_POP [ 15 ][ 20 ] 0014 OP_GET_LOCAL 0 [ 15 ][ 20 ][ 15 ] 0016 OP_GET_LOCAL 1 [ 15 ][ 20 ][ 15 ][ 20 ] 0018 OP_ADD [ 15 ][ 20 ][ 35 ] 0019 OP_PRINT [ 15 ][ 20 ] 0020 OP_POP [ 15 ] 0021 OP_POP
From the context of writing a script this implementation detail doesn't really matter. But since local variable are very slightly faster than global variables, it might be best to prioritize using them when possible.
Rescue Script Value Types
Before discussing more scripting features, it is important to understand the different value types supported by rescue script.
Numbers
The old version of rescue script distinguished between two types of numbers: whole number and floating-point numbers. The new version only stores floating-point numbers. So what happens to commands that only take whole numbers? For example, the camera.fixed command only takes whole number 1-3.
camera.fixed(1.9);
In the above snippet, you might expect the script to switch to fixed camera '2'. However, it will actually switch to fixed camera '1'. When converting from floating-point numbers to whole numbers, no rounding will take place. The decimal portion is just cut off. This is standard behavior in most programming languages, and it can actually be useful sometimes. But if you do want traditional rounding, I have added a native function for this, which I will discuss later in the post on functions.
The operations that can be performed on numbers are:
-x Negation x + y Addition x - y Subtraction x * y Multiplication x / y Division
Strings
Strings are the value type used to store text. They are called strings because they represent a string of characters. Strings must be defined between double quotes. If you want to store a double quote within the string, you must use something called an escape sequence. Escape sequences are a sequence of characters starting with a '\'. Common escape sequences are \", \n (new line), \t (tab), and \\ (backslashes need to escape themselves).
Strings only support one operation:
x + y Concatenation
Concatenation means stitching the strings together: "Hello" + " " + "World" = "Hello World". In rescue script, string concatenation will always occur as long as one of the operands is a string. This means it will convert the other operand into a string, then stitch them together.
10.5 + " Hello" = "10.5 Hello"; "Hello " + 10.5 = "Hello 10.5"; true + " Hello" = "true Hello";
Booleans
Booleans are values that store either 'true' or 'false'.
Booleans support the operations
!x Not (swaps true to false and false to true) x and y And (Returns true if both x and y are true) x or y Or (Returns true if either x or y is true)
In addition to these operations, there are a few operations that can take any value types as operands and results in a boolean.
x == y Equals (Returns true if x equals y) x != y Not Equals (Returns true if x doesn't equal y)
And a few operations that take numbers as operands and results in a boolean
x < y Less Than x > y Greater Than x <= y Less Than Or Equal To x >= y Greater Than Or Equal To
It is important to understand boolean operations because they are required for the conditions in if-statements and while-loops.
Null
Null values are a special value type that means empty or uninitialized. These values can be useful to flag if something went wrong or if the result of some function is 'nothing'. It is important to note that a variable will default to 'null' if it isn't assigned anything when it is declared. Also, functions will implicitly return null if there isn't an explicit return statement.
Functions
Functions in rescue script are actually stored as value types. This means you can assign functions to variables, then call those variables as functions. In fact, functions are actually already stored as global variables that are preloaded before the script starts. This behavior can be useful in a few scenarios.
For example, what if you wanted to give the user the option to use an AED or defib paddles. You could set a global variable (useAED = true), then check that variable with an if-statement every time you wanted to call a shock function. A more elegant solution would be to point a variable to different functions based on the user's choice.
Note in the above example I didn't fill the functions out. I just wanted to show the function declarations for clarity.

Anya is live and ready to show you everything. Watch her strip, dance, and perform exclusive shows just for you. Interact in real-time and make your fantasies come true.
Free to watch • No registration required • HD streaming
Choice Panel Demo
The choice panel is the feature that drove the creation of Rescue Script 2. I mentioned in my last post that I actually got the choice panel working on the old version of rescue script. However, I realized that this feature would heavily benefit from traditional scripting features. I will discuss how the choice panel and other scripting features work together further down, but first here's the basic syntax.
Opening the choice panel starts with a choice block and this is where the question text goes. A choice block must contain between 1 and 4 option blocks. I put the limit at 4 options for both UI reasons and because it's easier to work with in the vm. The option blocks start with the answer text, then all the lines within an option block will execute if the user selects that option at runtime. All the text that is displayed to the user uses the same text interpolation as dialogue text, so all the tags will work here.
Another thing about choices is that time stops while the choice panel is open. I decided this should happen so the user doesn't feel pressured to make a choice quickly.
As I mentioned, choice blocks work really well with other scripting features. An example of this is the aed shock loop in the demo video above. There I used a while loop to infinitely open the choice panel until the user was done using the aed. I also created a function to randomize the rescuer's dialogue during this loop.
Here's silly example I made when designing the system to show how something like this is done:
This is a small example, but there's a lot more that can be done with this. For example, someone could write a script that has a good ending and a bad ending. Then, if the user gets the bad ending, there could be a choice that lets them go back to that decision and try again. Or maybe there could be a choice to let the user decide if they want to use an aed or defib paddles. Then that choice could set a global variable that is checked every time there is a shock section in the script.
Anyway, I won't write a devlog this time. It was actually a lot harder to get the choice panel working in the new system than it was for the old system. However, it will be hard to explain why without some knowledge of some other features that I would rather explain in future posts.
Rescue Script 2 Overview
In my last post, I mentioned that I decided to rewrite the rescue script system from scratch. The new system introduces many new features, and I will make a bunch of posts later this week explaining them. This first post is an overview of the changes and my reasoning for rewriting the system.
It all started when I had an idea for a new feature, which I will reveal in tomorrow's post. I actually implemented this feature with the old system, and it wasn't too hard to get working. However, I realized this feature would really benefit from traditional scripting language features. Things like variables, functions, flow control, and expressions. When I first started working on rescue script, I viewed it more like a movie script than a coding script, so I didn't really see the point of adding these traditional scripting features. However, this new system is significantly more powerful than the old one.
Rescue Script 2 introduces global variables, local variables, functions, if statements, while loops, and a few native functions. Of course, none of these would work without expressions, which will be the main focus of this post.
An expression is a piece of code that resolves to a value. It can be as simple as a single number, or it could be something like:
1 + 2 * 8 - (10 / 2)
This snippet as a whole is an expression, but each of the numbers is also an expression. So expressions can be viewed as a tree that contains many other expressions within them. The important thing to note here is that they resolve to a single value. In this case, the answer is 12. Note that operator precedence is also taken into account.
The old rescue script system didn't allow expressions. You couldn't write something like:
heart.bpm(1 + 2 * 8 - (10 / 2));
This is because the old system stored all the command data at compile time. And expressions can't be decided until runtime. To be fair, the above example could be resolved at compile time. But what if you had something like this:
let heartRate = 0; if random(0, 1) > 0.5 { heartRate = 150; } else { heartRate = 50; } heart.bpm(heartRate);
This snippet shows some of the new syntax as well as a native function, but I will explain those later. The point here is that the script doesn't know the value of the 'heartRate' variable until it actually runs. It could either be 150 or 50, depending on the return value of the 'random' native function.
So you can think of the old system as being completely static and decided at compile time, while the new system is dynamic and the output is decided at runtime. I will finish off this overview with some negatives of the new system.
Negatives
In one of my recent posts, I claimed that semicolons were no longer required. That's no longer true. It makes the script a lot easier to parse when each statement ends in a semicolon. So sorry about that bit of misinformation.
Since the program is mainly resolved at runtime, this introduces runtime errors. In the old system, all errors could be reported when the script was compiled. Now the errors might not show up until testing. This is a big downside, however I have implemented some features to make testing easier. I will make a whole post about errors and testing where I will explain more.
The old system had two methods of running a script. You could either run the whole thing by pressing 'F3', or you could run it line by line by pressing 'K'. However, now the line by line mode doesn't really make sense. If you read the devlog segment below you will see why, but basically each line of the script turns into many bytecode instructions and you would have to press 'K' many times for anything to happen. So I removed this mode and now scripts can only be auto-run by pressing 'F3'.
Devlog
The rest of this post will explain some of the technical details around the scripting system. However, this info probably won't help when writing your own rescue script, so feel free to skip if you don't care.
So far I've shown some of the new features, but that doesn't explain why I had to start from scratch. I could have implemented these features into the old system. The reason I started from scratch was because of execution speed.
The old system was a tree-walk interpreter. It parsed the user's script into an abstract syntax tree, then when the script was run it would 'walk' through the tree executing each node. The problem here is that the code was implemented in C# (Unity's main programming language), and trees are usually made using objects that are stored on the heap. That means that stepping through the tree was slow because objects on the heap have low cache locality. Storing the script as a tree also has more overhead, takes up more space, and adds more GC pressure. If you don't understand anything I just said, the point is that the old system was slow.
The fact that the old system was slow didn't really matter. Since old rescue scripts were just a list of commands, execution speed wasn't that important. This was especially true since all the required data was resolved at compile time. Now that the new system needs to resolve things at runtime, speed is a concern.
This is why I rewrote the system as a bytecode interpreter. Instead of parsing the user's script into an abstract syntax tree, it instead parses the user's script into bytecode.
Bytecode is a list of instructions, named that way because they only take 1 byte (this means there can only be 256 instruction types, as a byte ranges from 0-255). These instructions are called opcodes, and since they are just an array of bytes it means they have excellent cache locality. It also means the compiled programs are very small. For example, my recent 'Bedroom' script turned into only a kilobyte of bytecode. For example, the previous snippet:
1 + 2 * 8 - (10 / 2);
Turns into the following bytecode:
0000 1 OP_IMMEDIATE 1 0002 | OP_IMMEDIATE 2 0004 | OP_IMMEDIATE 8 0006 | OP_MULTIPLY 0007 | OP_ADD 0008 | OP_IMMEDIATE 10 0010 | OP_IMMEDIATE 2 0012 | OP_DIVIDE 0013 | OP_SUBTRACT
You may be able to follow the above code and get 12 as the answer. If not, that's okay since we're missing a key element: the stack. Bytecode is interpreted by a 'virtual machine' (which I will abbreviate VM). VM's usually have a form of storage to work with values, in our case that is the stack. A stack is just a list of values. However, a stack follows a Last-In-First-Out principle. That means the last value added is the first to be removed. You can think of a stack like stack of plates. You can't access the plate on the bottom without first removing all the plates on top of it.
Here is the above example with a stack trace showing the stack as it executes. The values on the stack are the numbers between the square brackets.
0000 1 OP_IMMEDIATE 1 [ 1 ] 0002 | OP_IMMEDIATE 2 [ 1 ][ 2 ] 0004 | OP_IMMEDIATE 8 [ 1 ][ 2 ][ 8 ] 0006 | OP_MULTIPLY [ 1 ][ 16 ] 0007 | OP_ADD [ 17 ] 0008 | OP_IMMEDIATE 10 [ 17 ][ 10 ] 0010 | OP_IMMEDIATE 2 [ 17 ][ 10 ][ 2 ] 0012 | OP_DIVIDE [ 17 ][ 5 ] 0013 | OP_SUBTRACT [ 12 ]
Now you might be able to see why I removed the 'K' script run option. You would have to press 'K' 9 times just to get the output of the single expression.
Okay, I know this is a lot but there is one more key piece of bytecode. For that, we will need a new example;
print "Hello " + "World. Here is a decimal number: " + 7.5;
And the bytecode with the stack trace:
0000 1 OP_CONSTANT 0 'Hello ' [ 'Hello ' ] 0002 | OP_CONSTANT 1 'World. Here is a decimal number: ' [ 'Hello ' ][ 'World. Here is a decimal number: ' ] 0004 | OP_ADD [ 'Hello World. Here is a decimal number: ' ] 0005 | OP_CONSTANT 2 7.5 [ 'Hello World. Here is a decimal number: ' ][ 7.5 ] 0007 | OP_ADD [ 'Hello World. Here is a decimal number: 7.5' ] 0008 | OP_PRINT
You may note the different instructions here for pushing values on the stack. The last example used 'OP_IMMEDIATE' and this one uses 'OP_CONSTANT'. The 'OP_IMMEDIATE' instruction is used to push a single byte onto the stack. This can be any whole number from 0-255. For anything else, OP_CONSTANT is used to push a value from a data structure called the constant pool.
When a script is parsed, all constant values (other than immediates) are added to the constant pool. These values are then able to be pulled out by index. In the above line, 'OP_CONSTANT 0 'Hello ' grabs the value from the constant pool at index 0, which is the string 'Hello '. It also needs to grab the decimal value '7.5' from the constant pool since that can't be stored in a single byte.
The other thing to note about this example is that rescue script does allow string concatenation. It also implicitly converted the decimal value 7.5 into a string. I will talk about this more when I write the post on variables and value types. I guess it also shows the new 'print' command, which I will also explain later.
I think this is enough tech-talk for now. If you're interested in learning more, I highly recommend the book "Crafting Interpreters", which you can read online for free here: 'https://craftinginterpreters.com/'. I followed this book a lot when implementing both the new and old rescue script systems.
Rescue Script Custom Camera
I actually implemented the features discussed in this post a couple weeks ago. I held off posting about them because I later decided to rewrite the rescue script system from scratch. Soon I will make a bunch of posts about what I'm calling: "Rescue Script 2". But I figured it would be better to post about the camera features before I get into all that.
Custom Cameras
One of the big limitations with current scripts is how they handles custom cameras. When I first added the custom camera option to the game I expected to use it a lot more. But in most of my scripts I only use custom cameras for establishing shots or at the end of the scene. There are two reasons for this. The first is the limitation of only having three custom camera angles at a time. If you use one for the beginning shot and one for the end, that only leaves one extra camera angle to use. The bigger problem is that they don't really work for other people running the script. Unless someone reads the script before playing it themselves, there's no indication that they need to set up custom cameras beforehand. And even if there was, they wouldn't know where to place each of the cameras.
The solution is the new "setCustom" command, which looks like:
camera.setCustom(1, -0.5421, 0.7842, -0.8323, 36.8766, 27.5444, 0.0000, 30.0000);
This command allows a custom camera to be set by script. This solves both of the above problems, but introduces a new one. What are all those numbers? The first value is the index of the camera to set (1-3), the next 3 values are for the camera's position, the next 3 are for the camera's rotation, and the last value is for the camera's fov. But no one's going to type all that out manually. Instead, a new prompt will pop up whenever you set a custom camera. This prompt will allow you to copy the command for that camera's position, rotation, and fov into the system clipboard. Then its super easy to paste it into the script your working on.
Camera Paths
After adding this feature, I thought it would be really cool if some camera motion could be added. The problem here is that adding camera motion would require twice as many inputs as the above command. However, I realized that the motion could instead be defined using two custom cameras. The new path command looks like:
camera.path(1, 2, 4.5);
The first value is the index of the first custom camera, the second value is the index of the second camera, and the third value is the duration of the camera motion.
This command will interpolate between the position, rotation, and fov of the given custom cameras using an InOutQuad easing for smooth motion. This works really well for simple pans and zooms. It can also work for more complex motion but the results will vary. Still, its pretty fun to experiment with. Note that the script will immediately continue running, rather than wait for the motion to finish. I made this decision since its easy enough to add a wait afterward since the duration is already known. Otherwise you would have to use an async block, which could make things more complicated.
MTM WIP
I've been trying to update the mtm animation, but I'm not sure how I feel about it. I've spent a long time tweaking the poses, but before I continue working on it I thought it would be good to get some feedback.
The Negatives
Ignore the fact that the fingers for the nose pinch aren't placed right. They were placed better in the blender file, but unity uses animation retargeting that can mess with the position of things. I will need to tweak the animation to compensate for this.
I didn't think about the hair clipping through the hand issue when making the animation. I'm not sure how much of a big deal this is.
The rescuers right arm clips into the victim's chest a bit. I've tried moving the elbow around, but I couldn't find a pose where this wasn't an issue.
This version of the animation blocks more of the view from certain angles compared to the original animation.
The Positives
The original animation kept the rescuers knees in the same place as the cpr animation, making the transitions easier. However, it also meant they had to contort their body more. For this one, I moved their knees to better fit the motion, which looks a bit better. It will make creating the transitions more difficult when I get to that.
I think the nose pinch does look more 'correct' and more intimate than the original animation.
The victim's chest rise is more subtle, which I think looks better, though I think I made it a little too subtle.
Anyway, let me know if you think I should refine this animation more or if I should create something closer to the original mtm animation.
Which do you prefer?
Nose Pinch
Original Animation
Rescue Script QOL Updates
Now that I've written a few rescue scripts, I've noticed a few areas that can be improved. The following changes will increase script readability and make writing scripts faster and easier.
Removed Semicolon Requirement
Semicolons are no longer required at the end of a command. I made this change because the semicolons don't fit well with the other changes I will discuss below. And it turns out, the semicolons weren't actually doing anything. After I removed the requirement, the script parses exactly the same without any issues. So now the compiler will ignore any semicolons, which means that old scripts will still work fine but new ones don't need to use them.
Shortened Dialogue Command
In old scripts, a dialogue command looked like:
dialogue.label(R).text("Hello, here's some example dialogue.");
This was fine on its own, but it made blocks of dialogue hard to read. It was also annoying to write out, and copy-pasting the command often interrupted the writing flow. The original reason I designed the command this way was because I wanted all commands to behave the same. This made the code for the compiler really simple and elegant. In order to keep it that way, I considered shortening the command to:
d.l(R).t("Hello, here's some example dialogue.");
This makes the command faster to write, but it increases the visual noise and doesn't solve the root problem. And since dialogue is the main point of the rescue script system, I realized I needed to sacrifice the compiler's simplicity for the sake of a better user experience. So I ripped all of the extra noise out of the command. Now writing dialogue is as simple as:
R "Hello, here's some example dialogue."
This is so much more readable, and writing the dialogue is so simple that you no longer feel compelled to copy-paste it from a previous line. I've already updated the recent Bedroom script to this new system, and its so much nicer to look at. I also added a new dialogue label type. Using an '_' will make the dialogue box use the same label as the previous one. This further improves the readability of blocks of text. Compare this example of the old system:
dialogue.label("Narrator").text("I am the narrator and I'm going to say some example text."); dialogue.label("Narrator").text("Here I go, typing out a bunch of words."); dialogue.label("Narrator").text("And here's even more words to read.");
and the new system:
"Narrator" "I am the narrator and I'm going to say some example text." _ "Here I go, typing out a bunch of words." _ "And here's even more words to read."
Shortened Wait Command
Now that I've discarded the compiler's simplicity to shorten the dialogue command, I figured I might as well shorten some other commands while I'm at it. And the most obvious command to target was the script.wait command, which used to look like:
script.wait(1.5);
Now it's as simple as:
wait 1.5
This isn't as much easier to write than the new dialogue command, but it's still a big improvement considering how often I use the wait command. Timing is so important when writing rescue scripts, and I litter this command everywhere. So even a small change in readability can make a big difference.
Asynchronous Blocks
The previous changes were designed around simplicity. This feature arguably makes things more complicated, but it's also very powerful. Before I explain how it works, I need to explain the problem it solves. If you've played through my previous scripts, you'll know that I like to put dialogue while the character is doing something (ex, during chest compressions). In the script, this used to look like:
dialogue.label(R).text("Come on, [victim]").next(); cpr_side.repeat(15).rate(110, 105); dialogue.label(R).text("I hope I'm doing this right.").next(); cpr_side.repeat(15).rate(105, 120);
The .next() dialogue option would cause the script to skip waiting on it and immediately continue to the next line. However, splitting up the cpr_side command into two parts created a small, but noticeable hitch. This is very obvious in the Office3 script, if you've seen that. To solve this problem, I've recently introduced the .delay() dialogue option, which waits for the given seconds before showing the dialogue. Using this option, the above commands look like:
dialogue.label(R).text("Come on, [victim]").next(); dialogue.label(R).text("I hope I'm doing this right.").delay(8); cpr_side.repeat(30).rate(110, 105, 120);
This option helps avoid splitting up the cpr_side command, removing the hitch. But what if I wanted to change the camera-angle mid compressions? Or if I wanted to change the heart rhythm, or enable a condition? I didn't want to add the .delay() option to every possible command. Not to mention, neither the .next() or the .delay() option works with the new short-form dialogue. And while the old dialogue form still works, mix and matching the old and new forms would hurt readability.
To solve this problem, I'm introducing async blocks. Using this new feature, the above example looks like:
async { R "Come on, [victim]" wait 4 R "I hope I'm doing this right." } cpr_side.repeat(30).rate(110, 105, 120)
An async block works by splitting off from the scripts main time flow. All the commands within a block operate on their own time flow, running alongside the main script. So the first command within the async block runs at the same time as the cpr_side command. This means the first line of dialogue doesn't need the .next() command. You may notice that the wait is only 4 seconds, compared to the previous .delay()'s 8 seconds. This is because the first dialogue isn't skipped, and it takes around 4 seconds to run.
Hopefully you can see how powerful these async blocks can be. Any command can run inside them, like camera, heart, and condition commands. And these blocks allow the new forms of dialogue without any issues. In fact, async blocks can even be nested, although I'm not sure why someone would want to do that.
My only issue with these blocks is that a bit of clarity is lost. In the above example, its not clear that the async block is meant to go with the cpr_side command. However, I realized readability can be improved by shoving everything on one line, like this:
async { R "Come on, [victim]"; wait 4; R "I hope I'm doing this right." } cpr_side.repeat(30).rate(110, 105, 120)
This makes it more clear that the async block runs with the cpr_side command. I added in semicolons above to distinguish between the commands on a single line, but like I said before, these don't actually do anything. The above example is just a formatting decision that I plan to use on my own scripts, but feel free to format things however you want.
Anyway, these are the changes I've made so far. I've updated the recent Bedroom1 script to account for these changes, and I will post it on my discord if you want to look at it. It won't actually run, since I haven't released the update yet, but I'm looking for feedback on these changes so if you have ideas for further improvements let me know.

Anya is live and ready to show you everything. Watch her strip, dance, and perform exclusive shows just for you. Interact in real-time and make your fantasies come true.
Free to watch • No registration required • HD streaming
Rescue Theater 0.8.1 Release
I just released a new version of Rescue Theater. Download it here.
Change Log:
Characters:
Updated Audrey and June.
Wrote custom shaders for the characters. These should look the same as the old ones.
Character customization menu now allows changing of underwear colors. Press 'Y' to open this menu.
Character customization menu now allows changing of face, skin, and underwear textures.
Character customization menu has a new color picker that is more intuitive and contains presets for fast color selection.
Environment:
Updated lighting in Gym scene.
Bug Fixes and Performance:
Fixed character face color clipping through the character's eyes when they are closed.
New shaders should have improved performance.
The new shaders allowed the clothing system to be revamped, which has reduced the game's file size by ~30%.
Character Customization Part 3
I wasn’t originally planning on working on the character customization for the 8.1 update, but I got a bit distracted after being made aware of a bug with the facial discoloration system. I was planning on making a separate post about this, but I never got around to it so I’ll try to write a quick explanation.
After the 8.0 release, there was a bug report about the face color effect clipping through the eyes when they are closed. I almost never use the eyes-closed option, so I never realized this problem before. The way this effect used to work is that I made transparent copies of the face, moved the vertices out a bit along their normals, then applied the blush textures. This way the effect was layered onto of the face.
This solution was always a bit hacky. The proper way to accomplish this effect would be to layer the textures within the shader. However, the default vroid shaders (called mtoon) are very complicated and I didn’t want to mess with them. After receiving this bug report, I realized that I would need to seek a shader solution after all. So I looked back at the mtoon shaders and I realized they have a lot of features I wasn’t actually using. Instead of trying to shove texture layering into these complicated shaders, I decided to design my own from scratch.
Of course, writing my own shaders took a while. I had to learn how to do lighting in unity’s urp, match the characters appearance to the old mtoon shader, and get the outlines working. Then I could finally fix the face issue by adding texture layering to the shader.
With that problem solved, I realized I could add the same layering system to the character’s body shader. This allowed me to overhaul the clothing system, and allows for things like changing underwear colors and textures. It also introduced a lot of issues with things like color-banding and compression, but I’ll spare the details on how I handled that.
Then I had some ui problems to deal with. You may remember the old ui had separate tabs for the hair and eyes, but I didn’t want to add more tabs for the underwear. Instead I created a custom color picker using the hsv format similar to other programs. This color picker also has some presets that allow for fast color changes. More importantly, it allowed me to compress all the color options to a single tab of the menu.
I also thought it could be cool to swap out textures. This was originally only planned for the underwear, but then I realized I could allow swapping face and skin textures as well. This may not be too useful at the moment, as many skin and faces are duplicates, but when I update more characters I will try to add more unique features like tattoos, piercings, etc. If anyone has some ideas for things they’d like to see on skin or face textures, let me know.