VScript library for rapid and comfortable prototyping of Portal 2 mods.
The focus of this project is to provide tools that assist in developing Portal 2 VScript mods faster and much more comfortably than through vanilla VScript. This involves adding syntactic sugar, new features, employing various workarounds for missing features or fixing broken ones through entities often specific to Portal 2. While ppmod strives to be performant, this does not come at the cost of reduced ease of use.
In other words, ppmod makes Portal 2's Squirrel feel less like a cheap hack, and more like a native game interface.
Since ppmod version 4, the environment is expected to be as clean as possible, without any instantiated entities or vectors. This can be achieved by placing ppmod.nut into scripts/vscripts and calling it at the very top of mapspawn.nut, after making sure that the current scope has server-side control:
// File: "scripts/vscripts/mapspawn.nut"
if (!("Entities" in this)) return; // Quit if the script is being loaded client-side
IncludeScript("ppmod"); // Include ppmod as early as possibleIncluding ppmod in an already populated environment will still work at the cost of additional vector metamethods and entity method abstractions - that's the syntactic sugar mentioned earlier. This will get logged to the console as a warning, but is technically harmless if these features are unused.
Setting up and working with an environment like this for the first time can be overwhelming, so here's some boilerplate to help you get started.
This script will spawn a red cube in front of the player's head, and make it print to the console once it gets fizzled. This should provide a solid example that you can then play around with to get an idea of what it's like to work with ppmod.
// File: "scripts/vscripts/mapspawn.nut"
if (!("Entities" in this)) return;
IncludeScript("ppmod");
// This function is called whenever a map is fully loaded
// We wrap it in async() to make it more comfortable to use asynchronous functions inline
ppmod.onauto(async(function () {
// Retrieve additional player info, like eye position and angles
local pplayer = ppmod.player(GetPlayer());
yield pplayer.init();
// Props cannot be created with the CreateByClassname method, so we use ppmod.create instead
yield ppmod.create("prop_weighted_cube");
local cube = yielded;
// Teleports the new cube to 64 units in front of the player's eyes
local pos = pplayer.eyes.GetOrigin() + pplayer.eyes.GetForwardVector() * 64;
cube.SetOrigin(pos);
// Colors the cube red with the "Color" input
cube.Color("255 0 0");
// Connects a script function to the cube's "OnFizzled" output
cube.OnFizzled(function () {
printl("The red cube has been fizzled!");
});
// For other inputs/outputs, refer to: https://developer.valvesoftware.com/wiki/prop_weighted_cube
}));These are essential utility functions, classes and methods that Portal 2's implementation of Squirrel and VScript should (arguably) have by default. This includes abstractions and shorthands of already existing features.
Returns the smallest / largest of two values.
min(0.5, 2) // Returns 0.5
max(0.5, 2) // Returns 2Rounds a number to the specified precision in digits after decimal point, 0 by default.
round(1.2345) // Returns 1.0
round(1.2345, 2) // Returns 1.23
round(5.5) // Returns 6 Vector(1, 2, 3) * Vector(2, 4, 8) // Returns Vector(2, 8, 24)
Vector(1, 2, 3) * 2 // Still returns Vector(2, 4, 6) Vector(1, 2, 3) / Vector(2, 4, 8) // Returns Vector(0.5, 0.5, 0.375)
Vector(1, 2, 3) / 2 // Still returns Vector(0.5, 1, 1.5) Vector(1, 2, 3).equals(Vector(1, 2, 3)) // Returns true
Vector(1, 2, 3).equals(Vector(4, 5, 6)) // Returns false "The vector is: " + Vector(1, 2, 3) // Returns "The vector is: Vector(1, 2, 3)" "The vector values are: " + Vector(1, 2, 3).ToKVString() // Returns "The vector values are: 1 2 3" Vector(10, 5, 10).Normalize() // Returns Vector(0.666667, 0.333333, 0.666667)
Vector(10, 5, 10).Normalize2D() // Returns Vector(0.894427, 0.447214, 0)The pparray class implements some additional array features not present in Portal 2's version of Squirrel. It can be initialized by either providing a size for a new array (and optionally a value to fill it with) or an existing array.
local arr = pparray([1, 2, 3]); printl(arr) // Prints "[1, 2, 3]" arr.join(" - ") // Returns "1 - 2 - 3" arr.indexof(2) // Returns 1, which is the index of the first element with value 2. arr.find(function (a) {return a >= 2}) // Returns 1, which is the index of the first element to pass the compare function. arr.includes(4) // Returns false, because the array does not contain the value 4.The ppheap class implements a priority queue data structure using a heap. It supports basic heap operations such as inserting elements and retrieving the top element. The heap can be initialized with a maximum size and an optional comparator function. The default comparator constructs a min-heap.
local heap = ppheap(10, function(a, b) { return a > b }); // Constructs a max-heap heap.insert(5)
heap.insert(3)
heap.insert(10)
heap.size // Holds the heap size, which is now 3Retrieves the top element of the heap without removing it
heap.insert(10)
heap.gettop() // Returns 10
heap.remove()
heap.gettop() // Throws "Heap is empty" heap.insert(10)
heap.remove() // Returns 10
heap.remove() // Throws "Heap is empty" heap.isempty() // Returns true if heap is empty, false otherwise heap.insert(5)
heap.insert(3)
heap.insert(10)
heap.bubbledown(2) // Sifts down the element at index 2 (10) to its correct position in the heapThe ppstring class implements some additional string features not present in Portal 2's version of Squirrel. It can be initialized without arguments or by providing an existing string.
local str = ppstring("Hello world!"); str.split(" ") // Returns ["Hello", "world!"] str.replace("l", "L") // Returns "HeLLo worLd!" str.includes("world") // Returns trueImplements a JavaScript-like "promise" system for working with asynchronous operations.
ppromise(func)In Portal 2, there are numerous mutually unsyncronised threads, which can be a hassle to work with. Namely, console commands and entity actions are the most common offenders in generating asynchronous code. Historically, ppmod has used callback functions to accomodate for this, but since version 4, a "thenable" system was established for clarity and consistency.
Setting up a basic ppromise means wrapping a function with the ppromise constructor, which returns a ppromise instance. (Internally, these aren't actually classes or objects due to a workaround for a bug in Portal 2's Squirrel runtime.) Here is an example of a simple promise that resolves in 5 seconds:
local wait = ppromise(function (resolve, reject) {
ppmod.wait(function ():(resolve) {
resolve("5 seconds have passed");
}, 5);
});There are several ways of obtaining the result of a ppromise. The simplest by far is to attach a script to the then, except, or finally methods:
// Prints "5 seconds have passed".
wait.then(function (result) {
printl(result);
});
// Prints either the value given to reject(), or any errors caught by the ppromise.
wait.except(function (err) {
printl(err);
});
// Called when the promise resolves, regardless of outcome
wait.finally(function () {
// perform cleanup, etc...
});Any number of functions can be attached to each output. Only one of then or except gets called (whichever is encountered first), while finally gets called regardless.
You can also get the value and state of a ppromise directly, though this isn't recommended:
wait.state // One of "pending", "fulfilled", or "rejected"
wait.value // The value passed to either resolve() or reject()
Lastly, the value of a ppromise can be resolved inline via async functions.
To improve code clarity and reduce nesting, ppmod implements JavaScript-like async functions, which can resolve ppromises inline using the yield keyword, which in this case works similarly to JavaScript's await.
async(func)The simplest way of declaring such a function is to wrap it in async(). Here is an example of such a function that waits for ppmod.create to spawn an entity:
local createCube = async(function () {
yield ppmod.create("prop_weighted_cube");
local cube = yielded;
cube.SetOrigin(Vector(...));
...
});
// Can be called like a normal function
createCube();There are some important things to note here. Firstly, for context, here ppmod.create returns a ppromise that resolves to the created entity's handle. Secondly, yield on its own does not return this value. These async functions work by exploiting Squirrel's generators, which leads to admittedly hacky syntax. The value of the last yielded ppromise is instead stored in the yielded global.
Due to a bug in how Portal 2 handles restoring the script scope from save files, saving before an async function has finished running can lead to game freezes or crashes. It is therefore not recommended to use async functions in tick loops, and instead reserve them for one-time events like map loads or entity outputs.
These functions help with the Source entity input/output system. They mostly consist of streamlined versions of existing essential functions. If you've included ppmod within mapspawn.nut, you have access to the much more comfortable shorthands of these functions.
Searches for an entity by various criteria and returns its handle. Searches can be done by:
- Targetname
- Classname
- Model
- Entity index
- Position (more on that later)
Here are some examples of finding the first laser cube on sp_a2_triple_laser:
ppmod.get("new_box") // By targetname
ppmod.get("prop_weighted_cube") // By classname
ppmod.get("models/props/reflection_cube.mdl") // By model
ppmod.get(51) // By entity index (not recommended)Iterating over entities can be done by providing the previous entity's handle as the second argument:
local first = ppmod.get("prop_testchamber_door"); // First chamber door (usually entrance)
local second = ppmod.get("prop_testchamber_door", first); // Second chamber door (usually exit)Or, to iterate over every cube in a map:
local cube = null;
while (cube = ppmod.get("prop_weighted_cube", cube)) {
printl("Cube found at: " + cube.GetOrigin());
}As mentioned before, searches can also be performed by position:
ppmod.get(Vector(7687, -5863, 18)) // Starting position of the first cube on sp_a2_triple_laser
ppmod.get(Vector(7687, -5863, 18), 8) // Same search, but narrowed down to a 8 unit radius (default is 32)You can also apply a filter containing any of the previously mentioned search criteria to narrow down a position search:
local playerPos = GetPlayer().GetOrigin();
local radius = 1024;
// Search for a chamber door within 1024 units of the player
ppmod.get(playerPos, radius, "prop_testchamber_door");Checks whether the input argument is a valid entity handle. Performs a type check, class check, and calls the IsValid() method.
local cube = ppmod.get("prop_weighted_cube");
ppmod.validate(cube); // Returns true
cube.Destroy();
ppmod.validate(cube); // Returns falseRuns a callback function for every valid entity that matches the search. First argument is an array of ppmod.get arguments, the second argument is the callback function, which is provided each iteration's respective entity.
ppmod.forent(["prop_weighted_cube"], function (cube) {
printl("Cube found at: " + cube.GetOrigin());
});Performs a ppmod.get search in reverse. Useful for finding the last entity of a sequence.
ppmod.prev("prop_testchamber_door"); // Gets the last door in the map (as indexed by the engine)Fires an input to an entity, either by classname, targetname, entity handle, or ppmod.forent query.
ppmod.fire(target, action, value, delay, activator, caller)This functions very similarly to methods like DoEntFire or EntFireByHandle, with some additional quality-of-life changes:
- The
targetmay be either a string, entity handle, or an (array of) argument(s) passed toppmod.get. - Every argument other than the
targetholds a default placeholder value. - The
valueis automatically converted to a string.
This means that ppmod.fire can be used as a universal shorthand for firing inputs to entities. All of the following examples are valid:
ppmod.fire("prop_weighted_cube", "Dissolve", "", 2, null, null); // Fizzle all cubes in 2 seconds
ppmod.fire("named_cube", "Dissolve"); // Fizzle a specific cube
ppmod.fire([Vector(...), 128, "prop_weighted_cube"], "Dissolve"); // Find all cubes within a 128u radius and fizzle themSets a keyvalue for an entity provided by either classname, targetname or handle.
ppmod.keyval(entity, key, value)This functions similarly to __KeyValueFrom... methods, or the keyvalue format of the AddOutput input. The differences are:
- The
entitymay be provided as either a string, entity handle, or an (array of) argument(s) passed toppmod.get. - Value types are detected and converted automatically.
- The keyvalue is applied instantly, skipping the entity I/O queue.
Similarly to ppmod.fire, this functions as a universal shorthand for keyvalues. Here are some valid examples:
ppmod.keyval("prop_weighted_cube", "RenderColor", "255 0 0"); // Colors all cubes red
ppmod.keyval("cube1", "Targetname", "cube2"); // Changes the name of "cube1" to "cube2"
ppmod.keyval("weapon_portalgun", "CanFirePortal1", false); // Disables the portal gun's blue portal
ppmod.keyval([Vector(...), 128], "angles", Vector(-90, 0, 0)); // Rotates entities within a 128u radius to face directly upwardsSets an entity's spawnflags. This is really just an abstraction of the "SpawnFlags" keyvalue.
local trigger = ppmod.get(Vector(...), 32, "trigger_once");
ppmod.flags(trigger, 1, 2, 8); // Makes a trigger react to clients, NPCs and physics propsThe flags are provided as a variable number of arguments and are summed up to create the value set as the SpawnFlags keyvalue.
Connects an entity output to another entity's input. For more info on entity I/O, see here.
ppmod.addoutput(entity, output, target, input, value, delay, max);The arguments of this function are identical to those used in Hammer or those passed to the AddOutput input. Note that:
- The
entityandtargetmay be provided as either a string, entity handle, or an (array of) argument(s) passed toppmod.get. - Outputs are added instantly, skipping the entity I/O queue.
- Quotes and special characters are safe to use, with the exception of
\x1B.
Similar to its sister function ppmod.addoutput, but instead of firing an input when an output is received, a script function is called.
ppmod.addscript(entity, output, script, delay, max)The arguments here are similar to those in ppmod.addoutput, except for script, which may be either a string of Squirrel code, or a function. The scope of the attached function is provided with handles to self, activator, and caller.
Runs a script as the specified entity. Nearly identical to ppmod.fire(entity, "RunScriptCode", script), with the exception that the script can be either a string or a script function.
ppmod.runscript(entity, script)Sets the parent of the given child entity to a parent entity, joining their movement together.
ppmod.setparent(child, parent)The parent argument must be an entity handle, while the child can be provided as either a string, entity handle, or an (array of) argument(s) passed to ppmod.get. Setting the parent to a falsy value like null will remove any parent from the child. This acts as a cleaner alternative to the SetParent and ClearParent entity inputs.
Iterates over all of the children of an entity. This is contrary to CBaseEntity::FirstMoveChild, which returns just the first child in the hierarchy.
ppmod.getchild(parent, child)The parent argument expects an entity handle for the parent whose children are to be iterated over. The child argument may be omitted or null for the start of an iteration, or the handle of a child if continuing an iteration. The return value is null if no more children are found, or a handle to the next child in the iteration.
Note: Iteration does not start from the entity returned by FirstMoveChild. Therefore, to start an exhaustive iteration, pass null to get the first ordered child.
Sets an entity's input hook.
ppmod.hook(entity, input, script, max)The given script function gets called whenever the entity receives the specified input. If this function returns false, the input is discarded. Otherwise, it gets executed as per usual. This can be used to make specific inputs conditional, or to disable them outright. Note that input names are case sensitive, and typically use the CamelCase format, though that depends on how the input is fired. The scope of the attached function is provided with handles to self, activator, and caller.
Many of the functions documented above are also implemented as methods in most entity classes (assuming that ppmod was included before entities were instantiated). This provides some syntactic sugar that makes entity code shorter and cleaner.
Keyvalues can be assigned as a property of an entity's handle.
local pgun = ppmod.get("weapon_portalgun");
pgun.CanFirePortal1 = false; // Disables the portal gun's blue portalNote that you cannot retrieve keyvalues this way.
Inputs can be fired by calling them as a method of an entity's handle. All arguments remain optional, and are ordered just like in ppmod.fire.
local cube = ppmod.get("prop_weighted_cube");
cube.Color("255 0 0", 2.5); // Colors the cube red after 2.5 secondsAlternatively, you can fire inputs by calling the Fire method of the handle.
cube.Fire("Dissolve"); // Fizzles the cubeOutputs can be added via the AddOutput method of an entity's handle. Arguments remain the same as with ppmod.addoutput.
ent.AddOutput(output, target, input, value, delay, max)Similarly, scripts can be added via the AddScript method.
ent.AddScript(output, scr, delay, max)Scripts can also be added by calling the output as a method of the entity handle and providing a function as the first argument.
cube.OnFizzled(function, delay, max)The RunScript method acts as a shorthand of ppmod.runscript.
ent.RunScript(script)Entities can be parented via the SetMoveParent method. Behavior is the same as with ppmod.setparent.
ent.SetMoveParent(parent)Note that the method GetMoveParent exists in Portal 2 by default, and can be used to retrieve the parent entity handle.
Similar (but not complimentary, see note below) to the built-in FirstMoveChild method, ppmod implements NextMoveChild for iterating over more than one child. Behavior is the same as with ppmod.getchild.
ent.NextMoveChild(child)Where child is either unset or null for the start of an iteration, or an entity handle for a child of the parent if continuing an iteration.
Note: Iteration does not start from the entity returned by FirstMoveChild. Therefore, to start an exhaustive iteration, pass null to get the first ordered child.
Hook functions can be added to inputs via the SetHook method. Arguments remain the same as with ppmod.hook.
ent.SetHook(input, script, max)The base SetOrigin, SetAbsOrigin and SetAngles methods are overridden to sanitize input arguments and to allow for seamless switching between component-wise and vector input.
By default, nothing prevents you from accidentally setting a coordinate or angle of an entity to nan or inf, both of which cause undefined behavior and crashes. These overrides will throw an exception if such values are detected.
Additionally, SetOrigin and SetAbsOrigin now accept component-wise input, and SetAngles now accepts PYR vector input:
ent.SetOrigin(Vector(x, y, z)) // Still valid
ent.SetOrigin(x, y, z) // Also valid
ent.SetAngles(p, y, r) // Still valid
ent.SetAngles(Vector(p, y, r)) // Also validIn Portal 2, GetVelocity and SetVelocity refer to the QPhys velocity values by default. These methods are overridden for props in ppmod to allow for similar behavior to what is expected. These methods remain unchanged for player entities.
The GetVelocity method returns a ppromise which resolves to a velocity Vector obtained by interpolating the position of the entity over two ticks:
// Prints the velocity of `ent`
ent.GetVelocity().then(printl);The SetVelocity method first obtains an estimate of the current velocity using the GetVelocity override above, computes the difference between that and the input, and then uses ppmod.push to apply the new velocity.
// Sets the velocity of `ent` to 200ups upward
ent.SetVelocity(Vector(0, 0, 200));These functions implement additional features to VScript's program control flow, like timers and intervals, essential for game programming.
Runs the given script after the specified time.
ppmod.wait(script, seconds, name)The script may be provided as either a string of VScript or a function. The time is to be provided in seconds, and can be either an integer or a float. The name argument is optional. If set, it names the underlying logic_relay entity, which can then be found and destroyed, aborting the timer.
Here is an example of using this function to fizzle a cube in 2 seconds, unless the player picks it up before then:
local cube = ppmod.get("prop_weighted_cube");
ppmod.wait(function ():(cube) {
cube.Dissolve();
}, 2.0, "wait_dissolve");
ppmod.addoutput(cube, "OnPlayerPickup", "wait_dissolve", "Kill");Note that this function returns a handle for the logic_relay entity, which can also be used to destroy the timer without having to give it a name:
local timer = ppmod.wait(...);
if (condition) timer.Destroy();Runs the given script repeatedly in the specified time interval.
ppmod.interval(script, seconds, name)The arguments for this function are nearly identical to those used for ppmod.wait, with the only exception being that the time is optional. If the interval time is not set (or if set to 0), the script will get called on every entity tick.
Here is an example of a script that hurts the player every 0.5 seconds if they aren't standing on a cube:
local player = GetPlayer();
ppmod.interval(function ():(player) {
local feetpos = player.GetOrigin() - Vector(0, 0, 18);
local cube = ppmod.get(feetpos, 8, "prop_weighted_cube");
if (!cube) {
player.SetHealth(player.GetHealth() - 20);
}
}, 0.5);Runs the given script for every console tick, or waits the specified amount of ticks to run a script.
ppmod.ontick(script, pause, timeout)The script may be provided as either a string of VScript, or a function. If pause is set to true (default), the loop will not run while the game is paused. If timeout is set to -1 (default), the loop will run indefinitely for every tick. Otherwise, this function acts as a timer, and will wait for the specified amount of ticks to pass before running the script once.
Here is a simple example that will print "Hello Portal 2!" to the console every tick, even when the game is paused.
ppmod.ontick(function () {
printl("Hello Portal 2!");
}, false);Note that it is generally recommended to use ppmod.interval or ppmod.wait instead, where applicable. This function will not preserve its ticks through save files, and large scale use of ppmod.ontick loops can theoretically cause console buffer overruns. The only recommended usecase for this function is for keeping time during game pauses.
Runs the given script once the map has fully loaded.
ppmod.onauto(script, onload)The script may be provided as either a string of VScript, or a function. The onload argument is optional (false by default), and when set to true, calls the provided script on not only the initial map load, but also any save file loads.
This function is essential for almost any gameplay modding, and will often appear at the top of many ppmod scripts, as most entities are not accessible until the map has fully loaded. In single-player, this means waiting for the logic_auto entity to fire. In networked co-op games, this function also waits for the remote player (P-body) to fully connect before calling the script.
Works around script timeouts by catching the exception they throw.
ppmod.detach(script, args)In Portal 2's implementation of Squirrel, scripts (queries) are given a limited time to run, so that a simple while (true) loop doesn't hang the game. For compute-intensive operations, this can mean that the script is aborted before calculations are finished. When this happens, the VM throws a Script terminated by SQQuerySuspend exception. This function catches that exception, and calls the given function again, passing it the arguments from the previous run. If a different exception is caught, ppmod traces it back to the line on which ppmod.detach was called. Note that some of the trace data is unfortunately lost this way.
The script argument expects a function which is passed one argument - a table. The args argument is this table, which is passed back to the function every time it is called. The return value of script is also the return value of the ppmod.detach call.
Here is an example of using this function to call a for loop which increments an integer until it overflows:
ppmod.detach(function (args) {
while (args.i >= 0) args.i ++;
printl("i overflowed to " + args.i);
}, { i = 0 });Note that this will most likely take some time to run, during which the game will freeze. Be careful not to leave infinite loops running like this, since the only other safeguard is, theoretically, a stack overflow.
Provides more information about and ways to interact with a player.
local pplayer = ppmod.player(player)
pplayer.init().then(function (pplayer) { ... })The constructor for this class expects one argument - the entity handle of a player. Some of its routines are asynchronous. To test whether the instance has fully initialized, await the ppromise returned by pplayer.init.
Returns a ppromise that resolves once the asynchronous routines have finished running.
local pplayer = ppmod.player(GetPlayer());
pplayer.init().then(function (pplayer) {
// Interfaces such as `eyes` and `gravity` are guaranteed to work here
});Holds the entity handle that was used to instantiate this pplayer instance.
local pplayer = ppmod.player(GetPlayer());
pplayer.ent == GetPlayer() // trueProvides accurate eye position and angles.
pplayer.eyes.GetOrigin() // Eye position
pplayer.eyes.GetAngles() // Eye angles
pplayer.eyes.GetForwardVector() // Eye facing vectorIn Portal 2, retrieving the player's angles directly will return the rotation of the player model, which differs significantly from the player's view angles. Instead, pplayer.eyes uses a logic_measure_movement entity, which can be referenced for accurate eye position.
Holds the handle of the logic_playerproxy used for listening to jumping/ducking
// Changes the portalgun bodygroup to show PotatOS
ppmod.fire(pplayer.proxy, "AddPotatosToPortalgun");Holds the handle of the game_ui entity used for listening to player movement inputs
// Enable the Freeze Player spawnflag
ppmod.flags(pplayer.gameui, 32);Returns true if the player is holding a prop, false otherwise.
pplayer.holding() // Returns true or falseAllows for listening to player actions. Each of these functions expects one argument - a function to attach. Multiple functions can be attached to one event.
pplayer.onjump- Fired when the player issues a jump input.pplayer.onland- Fired when the player lands from a jump or fall.pplayer.onduck- Fired when the player starts the crouching animation.pplayer.onunduck- Fired when the player finishes the uncrouching animation.
Here is an example of using pplayer.onjump to listen for jumps:
local pplayer = ppmod.player(GetPlayer());
// Note: this will fire for every jump input, including those issued mid-air
// To listen only for initial jumps, check that pplayer.grounded is true
pplayer.onjump(function () {
printl("The player has jumped!");
});Returns true if the player is in the process of ducking/unducking, false otherwise.
pplayer.ducking() // Returns true or falseReturns true if the player is on the ground, false otherwise.
pplayer.grounded() // Returns true or falseAllows for listening to player inputs.
pplayer.oninput(input, script)The input argument expects a string specifying the input command to listen for. The script argument can be either a function, or a string of VScript code. Note that only the inputs provided by game_ui are supported, namely:
+moveleftand-moveleft+moverightand-moveright+forwardand-forward+backand-back+attackand-attack+attack2and-attack2
Here is an example of using pplayer.oninput to listen for when the player has attempted to shoot a portal:
local pplayer = ppmod.player(GetPlayer());
// Note: this will fire for every +attack input, even if the player can't shoot portals
pplayer.oninput("+attack", function () {
printl("Portal shot attempted!");
});Changes the player's gravity without affecting the gravity of other players or entities.
pplayer.gravity(gravity)One argument is expected - a multiplier for the strength of the player's gravity. A value of 1 will leave it unchanged, a value of 0 will disable gravity entirely, a value of 2 will make it twice as strong, and so on.
Note: This will throw if all async routines have not finished. To ensure this works, wait for pplayer.init() to resolve.
Changes the player's friction without affecting the friction of other players or entities.
pplayer.friction(factor)The friction argument holds the same meaning as the sv_friction console variable, which is hidden and inaccessible by default in Portal 2.
Here is an example of setting the player's friction to 2, which is half of the default value:
local pplayer = ppmod.player(GetPlayer());
pplayer.friction(2.0);Note that the calculations performed expect the value of sv_friction to be 4, which it is by default, and typically cannot be changed without unlocking the console variable via a plugin.
These functions provide ways to interact with the world and physical entities.
Creates an entity by running the given command and retrieves its handle.
ppmod.create(command, key)The command argument is the console command to run for creating the entity. Provided just an entity classname, the function uses the ent_create command. Providing a model path (any string ending with .mdl) will use prop_dynamic_create. The key argument is optional, and can be used to specify the exact criteria by which to search for the newly created entity. If not provided, the key is guessed from the input command. The function returns a ppromise, which resolves with the handle of the newly created entity.
In Portal 2, creating an entity with methods like Entities.CreateByClassname or CreateProp can be problematic, especially if the entity is complicated, is a physics prop, or isn't precached in some way. This is because these methods don't run initialization code often required for full functionality of many entities. Console commands are better in this regard, but pose a new issue - after an entity has been created, consistently getting a reference to it in VScript can be hard. This is why ppmod.create exists.
Here are a few examples of creating cubes with this function:
// The simplest method, just providing the classname
ppmod.create("prop_weighted_cube").then(function (cube) {
cube.SetOrigin(Vector(...));
});
// Using the game-specific command for spawning a companion cube
ppmod.create("ent_create_portal_companion_cube").then(function (cube) {
cube.SetOrigin(Vector(...));
});
// Using prop_physics_create to spawn a generic physics prop with the reflection cube model
ppmod.create("prop_physics_create props/reflection_cube.mdl").then(function (cube) {
cube.SetOrigin(Vector(...));
});Note that excessive back-to-back use of ppmod.create can cause the wrong handle to be returned. For cases where a large number of entities needs to be spawned, it is recommended to either spread the spawns across ticks in batches, or to use ppmod.give or CreateProp where applicable, assuming in the latter case that the model has been precached.
Creates a variable amount of entities under the player's feet.
ppmod.give(entities)The entities argument expects a table, where the slots are entity classnames, and each slot's value represents the amount of that entity to spawn. This function returns a ppromise which resolves to an array of entities created by this function.
While this functions a lot like the give console command, no console commands are actually used. Instead, the game_player_equip entity is spawned temporarily. This function is a great way to spawn many entities in bulk.
Here is an example of spawning a single cube at the player's feet with this function:
ppmod.give({ prop_weighted_cube = 1 }).then(function (ents) {
local cube = ents.prop_weighted_cube[0];
// ... do something with cube
});Creates a solid, invisible brush entity.
ppmod.brush(position, size, type, angles, create)The position argument is a Vector to the center of the brush. The size argument is a Vector containing the half-width of the brush along each axis. The type argument is the classname of the brush entity as a string. The angles argument (optional) expects a Vector, with the properties being pitch, yaw, and roll for X, Y, and Z, respectively. Lastly, the create argument (optional) is a boolean, specifying whether or not to use ppmod.create for creating the brush instead of Entities.CreateByClassname to work around unloaded entity features. If create is false (default), this function returns a handle to the newly created brush entity. If create is true, it creates a ppromise which resolves to a handle for the brush entity.
Here is an example of creating an invisible, outlined box at the center of the sp_a2_triple_laser chamber:
local brush = ppmod.brush(Vector(7808, -5629, 64), Vector(32, 32, 32), "func_brush");
ppmod.keyval(brush, "Targetname", "test_brush");
SendToConsole("developer 1");
SendToConsole("ent_bbox test_brush");Note that these brush entities cannot be assigned custom textures or complex shapes, as that requires a model for the brush to be precompiled into the map. Many brush entities may also not function as expected, even with create set to true.
Similar to ppmod.brush, creates a non-solid trigger volume entity.
ppmod.trigger(position, size, type, angles, create)The arguments and return values are the exact same as those used for ppmod.brush, refer to the documentation of that function.
The primary differences between this function and ppmod.brush are the output entity's spawn flags and the lack of collision. Every entity created via this function has its SpawnFlags keyvalue set to 1, which in most cases means that only players can trigger it. This can, of course, later be changed to account for other entities and props. Another notable exception is that if a trigger's type is set to trigger_once, it will automatically remove itself after being touched.
Here is an example of creating an invisible, outlined trigger at the center of the sp_a2_triple_laser chamber, which spawns a cube once the player touches it:
local trigger = ppmod.trigger(Vector(7808, -5629, 64), Vector(32, 32, 32), "trigger_once");
ppmod.keyval(trigger, "Targetname", "test_trigger");
ppmod.addscript(trigger, "OnStartTouch", function () {
ppmod.create("prop_weighted_cube").then(function (cube) {
cube.SetOrigin(Vector(7808, -5629, 128));
});
});
SendToConsole("developer 1");
SendToConsole("ent_bbox test_trigger");Creates a projected texture of the given texture and returns its handle.
ppmod.project(material, position, angles, simple, far)The material argument expects a path to the material (texture) to be projected. Self-illuminating textures work best. The position argument is a Vector to the origin of the projection. The angles argument (optional) expects a Vector containing the orientation of the projection with the properties being pitch, yaw, and roll for X, Y, and Z, respectively. If not provided, the entity will point directly downward (pitch 90). The simple argument (optional) is a boolean, denoting whether the projection is "simple" (i.e. applied flat to the nearest brush) or a true projection (false by default). The far argument (optional) specifies how far out the texture should be projected in Hammer units (128 by default).
Here is an example of projecting a red arrow on a wall in the sp_a2_triple_laser chamber entrance:
// Projections require shadows to be enabled
SendToConsole("r_shadows 1");
// Prevent any other lights from turning on (explained in docs)
ppmod.forent(["env_projectedtexture"], function (light) {
ppmod.hook(light, "TurnOn", function () { return false });
});
// Spawn the arrow projection
ppmod.project("signage/underground_arrow", Vector(7840, -5200, 81), Vector(0, 180), false, 70);Note that by default, Portal 2 only allows one projected texture to exist at a time. However, this check is only performed when the TurnOn input is called on an env_projectedtexture entity. By disabling this input via an input hook, we prevent this check from being run by, for example, chamber entrance triggers. Projected textures spawn turned on by default, therefore the input isn't actually necessary in most cases.
Applies decals on world brushes.
ppmod.decal(material, position, angles, far)The material argument expects a path to the material (texture) for the decal. The position argument is a Vector to some point on the brush where the decal should be applied. The angles argument (optional) expects a Vector containing the orientation of the projection with the properties being pitch, yaw, and roll for X, Y, and Z, respectively. If not provided, the entity will point directly downward (pitch 90). However, in practice, the angle seems to affect very little. The far argument (optional) limits how far away the decal can be cast. If not provided, set to 8 units by default.
While projected textures are best used for dynamically overlaying light projections on surfaces, decals can be useful for either their intended purpose (i.e. bullet holes, explosion dust), or for essentially changing the textures of entire brushes. Here is an example of making the back wall of the sp_a2_triple_laser chamber non-portalable:
// Ensuring decals are enabled
SendToConsole("r_drawdecals 1");
// This is required for making large decals less glitchy
SendToConsole("gpu_level 1");
// Create the decal to color the wall black
ppmod.decal("metal/black_wall_metal_002b", Vector(7968, -5440, 128));
// Create a brush to prevent portal placement
ppmod.brush(Vector(7968, -5408, 128), Vector(1, 96, 128), "func_brush");Note that if you plan to use materials which aren't intended for use as decals, it is recommended to set gpu_level to 1 or 0 (no real difference between these values), as otherwise the normal/bumpmap data of the material can cause the texture to flicker.
Traces a ray between two points, returning points of collision with the world or entities.
ppmod.ray(start, end, entity, world, portals, ray)The start and end arguments are Vectors to the start and end points of the ray, respectively.
The entity argument (optional), specifies which entities to test for collisions with the ray. This can be either a single entity handle, a non-array ppmod.foreach argument, or an array. If an array is provided, it may contain entity handles, ppmod.foreach arguments (including arrays), or sequential pairs of Vectors describing the origin and half-width of an arbitrary axis-aligned bounding box. If null or not specified, no collision with entities will be calculated.
The world argument (optional, true by default) is a boolean, denoting whether or not collisions with static world geometry should be considered.
The portals argument (optional) can be used to enable tracing rays through portals when provided an array of sequential prop_portal handle pairs. If null or not specified, rays will not teleport through portals.
The ray argument (optional) is mostly intended for use internally, though can also be used to reduce the calculations for multiple consecutive rays. More on this later.
This constructor returns an object with the following attributes:
fraction- a fraction along the line where an intersection occurred;point- the point of intersection;entity- the closest intersected entity (nullif none).
Here is an example of drawing a box at the location where a ray cast 256 units from the player's eyes intersects either the world or a cube each tick:
// Allow for drawing the intersection box through portals
SendToConsole("cl_debugoverlaysthroughportals 1");
ppmod.player(GetPlayer()).init().then(function (pplayer) {
ppmod.interval(function ():(pplayer) {
// Cast a ray 256 units forward from the player's eyes
local start = pplayer.eyes.GetOrigin();
local end = start + pplayer.eyes.GetForwardVector() * 256;
// Create an array containing all portals on the current map
local portal = null, portals = [];
while (portal = ppmod.get("prop_portal", portal)) portals.push(portal);
// Don't use portal passthrough if we don't have a full pair
if (portals.len() % 2 != 0) portals = null;
// This ray will collide with the static world, cubes, and will pass through portals.
local ray = ppmod.ray(start, end, "prop_weighted_cube", true, portals);
// If the ray didn't intersect anything, don't draw a box.
if (ray.fraction == 1.0) return;
// If the ray hit a cube, draw a green box. Otherwise, draw a red box.
if (ray.entity) {
DebugDrawBox(ray.point, Vector(-2, -2, -2), Vector(2, 2, 2), 0, 255, 0, 100, -1);
} else {
DebugDrawBox(ray.point, Vector(-2, -2, -2), Vector(2, 2, 2), 255, 0, 0, 100, -1);
}
});
});Note that only the axis-aligned bounding boxes of entities are checked for intersections, which may lead to slight mismatches with the actual model of some props.
As mentioned before, multiple similar calls to ppmod.ray can be optimized using the ray argument. Note that this is very situational, and probably won't matter in most cases. Regardless, here's how the value of ray is calculated:
local dir = end - start;
local len = dir.Norm();
local div = [1.0 / dir.x, 1.0 / dir.y, 1.0 / dir.z];
local ray = [len, div];Checks whether the oriented bounding boxes of two entities intersect.
ppmod.intersect(ent1, ent2)This function expects two arguments - the entity handles of the two intersecting entities. It returns a boolean - true if the OBBs intersect, false if they don't.
Checks whether a point is inbounds.
ppmod.inbounds(point)This function accepts one argument - a Vector to the point which the check is to be performed on. It returns a boolean - true if the point is inbounds, and false if it isn't. Note that this function can return false positives in super specific cases, but is generally safe to use for any of the campaign maps.
Checks whether a point is within line of sight.
ppmod.inbounds(eyes, point, fov)The eyes argument expects an entity handle to use as the subject for the check. A ppmod.player instance's eyes handle works well for this if the subject is a player. The point argument expects a Vector with absolute coordinates to the target point. If your line-of-sight check needs a range limit, you'll need to enforce that manually. The fov argument expects a number - the field-of-view in which the target is considered visible.
Note that this check performs no ray collision checks with dynamic entities. Additionally, very thin walls like the ones seen in the puzzles in act 3 of the singleplayer campaign may be considered see-through. Limiting the visible range may be a solution.
Creates a button prop and fixes common issues associated with spawning buttons dynamically.
ppmod.button(type, position, angles)The first argument is the classname for the button. All of the Portal 2 floor and pedestal buttons are supported. The position argument is a Vector to the spawn position of the new button. The angles argument (optional) expects a Vector, with the properties being pitch, yaw, and roll for X, Y, and Z, respectively. If not set, all angles will be 0. This function returns a ppromise, which resolves to a table. The contents of this table differ depending on the type of button spawned.
If a pedestal button is spawned, the ppromise resolves to this table:
GetButton()- returns thefunc_rot_buttonentity used for registering+usepresses;GetProp()- returns theprop_dynamicentity acting as the physical button;SetDelay(delay)- sets a delay in seconds before the button can be pressed again;SetTimer(enabled)- enables or disables the ticking timer sound, expects a boolean, doesn't actually delay the output;SetPermanent(enabled)- enables or disables locking the button down, expects a boolean;OnPressed(script)- attaches the given script string or function to the button'sOnPressedevent.
If a floor button is spawned, the ppromise resolves to this table:
GetTrigger()- returns thetrigger_multipleentity used for detecting collisions;GetProp()- returns theprop_dynamicentity acting as the physical button;GetCount()- returns the amount of entities currently holding down the button;OnPressed(script)- attaches the given script string or function to the event of the button being pressed.OnUnpressed(script)- attaches the given script string or function to the event of the button being released.
The reason this entity exists is that buttons created dynamically, even when using commands like ent_create, are broken in several ways by default. Instead, ppmod.button reconstructs the button from the ground up and simulates its behavior in VScript.
Here is an example of creating a button that opens the exit door on sp_a2_triple_laser:
ppmod.button("prop_button", Vector(7200, -5280, 0)).then(function (button) {
button.OnPressed(function () {
ppmod.fire("@exit_door-door_open_relay", "Trigger");
});
button.SetPermanent(true);
});Launches a physics prop in the given direction.
ppmod.catapult(entity, vector)The first argument is the entity to launch. The second argument is a Vector, the direction of which is used to control the launch trajectory and the length of which is used to set the launch speed.
Here is an example of launching every cube on the current map directly upwards:
ppmod.catapult("prop_weighted_cube", Vector(0, 0, 400));Note that you might first have to ensure that the prop you're launching is not asleep. In most cases, this can be done via the Wake input.
Applies a directional force to a prop, similar to what SetVelocity does on a player.
ppmod.push(entity, vector)The first argument is the entity to push. The second argument is a Vector, the direction of which is used to control the push direction and the length of which is used to set the push force in units per second.
Here is an example of adding a ~400ups upward force to every cube on the current map:
ppmod.push("prop_weighted_cube", Vector(0, 0, 400));Note: this function is different from ppmod.catapult, as the input vector more accurately represents the force applied, and the magnitude of this force is approximately scaled from units per second. This function also calls Wake and EnableMotion on the input entity, which ensures that the velocity is applied even if the prop is asleep.
Also note: The console variable portal_pointpush_think_rate is modified to increase point_push reliability.
These functions provide ways to interact with the game and the player's system outside of the world and its physics.
Displays text on a player's screen.
ppmod.text(string, x, y)All arguments for this constructor are optional. The string argument sets the string of text to display, while x and y position it on the screen relatively. These should be float values from 0 to 1, or -1 if the text is to be centered on the respective axis. An instance of ppmod.text has the following methods:
GetEntity()- returns thegame_textentity created by the constructor.SetPosition(x, y)- adjusts the position of the text using the same system as the constructor.SetText(string)- changes the text to be displayed.SetSize(size)- expects a value between 0 and 5, with those being the smallest and largest text channels respectively.SetColor(color1, color2)- both values are strings of form"R G B", with the second one being optional and used only as the transition color for fading, if fading is used.SetFade(in, out, fx)- sets fade in/out time in seconds. The third argument is an optional boolean - iftrue, the text will fade in letter-by-letter instead of all at once.Display(hold, player)- displays the text for the specified amount of seconds to the given player. Both arguments are optional - ifholdis not set, the text will display for one tick, and ifplayerisnullor unset, the text will display for all players simultaneously.
Creates a console command alias for calling a script function
ppmod.alias(command, script)The first argument is the command to alias - this can be any arbitrary string, or an existing command. The second argument is the script function or string to execute whenever the alias is called. Note: aliases cannot be cleared, only overwritten by other aliases.
Here is an example that aliases the +mouse_menu bind (the F key by default) to toggle between zero/normal player gravity:
local data = {
pplayer = ppmod.player(GetPlayer()),
factor = 1.0
};
ppmod.alias("+mouse_menu", function ():(data) {
data.factor = fabs(factor - 1.0);
data.pplayer.gravity(data.factor);
});