Scripting - Hirato/lamiae GitHub Wiki

This is about the scripting system surrounding the RPG subsystems of Lamiae, for a more general scripting guide, see CubeScript.

Signals

Signals are the heart and soul of the scripting system. Scripts define a great many signals, which are then implicitly called from the game, or explicitly by the game's creator. The signals can have almost any target, as these are used universally and all over the RPG subsystems.

Signals can be defined via the r_script_signal configuration command, as well as r_script_signal_append and both are in the form command "signal" [body]. For a more detailed explanation on defining signals, see Configuration.

Defined signals when called, are done in the order they're created. If for example I was to call a signal named "foo" and I defined and subsequently appended twice. The initial declaration will be called, followed by the first appended, and then the second.

//numbered in the order of execution.
r_script_signal "foo" [ echo 1 ]
r_script_signal_append "foo" [ echo 2 ]
r_script_signal_append "foo" [ echo 3 ]

r_script_signal will override any additions you've made to the relevant signal.

Arbitrary signals can be sent from the game at any time, via the command r_signal. This allows users to call and send any arbitrary signal to objects in the game world. The command is defined as follows...

prototype:
	void sendsignal(const char *sig, const char *send, const char *rec, int prop);
	COMMANDN(r_signal, sendsignal, "sssi");

script:
	r_signal "mysignal" source target 1

The parameters are in the order of the signal's name, who sent it (aka, the actor in the execution context), who will receive it, and a boolean which defines if it should propagate downwards. The protptye above gave this an explicit value of 1, You usually don't want to do this. If you leave out the target, the signal is sent to everything, and also to the cutscene subsystem.

The propagation goes in the order of Map --> Entities --> Inventory/Equipment.

Reserved Signals

In addition to user defined signals, there are some reserved signals, these are called by the game itself over time, and users should therefore avoid passing them about for their own sanity.

  • spawn
  • collide
  • update
  • ai update (yes, there's a space)
  • load
  • enter
  • attacksound (note, hack)
  • level (scripted)
  • equip
  • death
  • hit
  • interact
  • import #
  • resurrect
  • arrived

Spawn

The signal is sent to an entity when it spawns for the first time.

Collide

When two entities collide, both receive a collide signal with the other party as an actor

Update

This signal is sent at the start of every frame. You should avoid using this if at all possible due to the overhead cost of running complex scripts every frame.

AI Update

This is a special, throttled version of the update signal specifically for AI. This is only sent to Critters who are alive, and this is throttled by the value of the variable, r_aiperiod.

r_aiperiod is a variable which defines the average time between invocations of the "ai update" signal, the default value is 100, and this translates to 50-150ms elapsing between updates.

This is meant to reduce the frequency of AI Updates especially as they're fully scripted and running this, even as bytecode, has some overhead.

Load

This signal is sent to a map when it is loaded for a first time. Maps with the MAP_VOLATILE flag can receive this one multiple times. This signal does not recurse.

Enter

This signal is sent to a map whenever it is loaded. It does not recurse. It is ideal to use this to track any state changes.

Attack Sound (hack)

Level (scripted)

This signal is sent to an entity as it levels up. This by default is used to print the specifics of the levelup (ie, which level and how many points are available for spending). This signal is provided by the default leveling subsystem.

This can also be used to execute level up schemata, like those found in Arcanum.

Equip

This signal is sent to the equipped item with a reference to the entity that equipped it. This can be used to unequip items that lack certain quest requirements, among other things.

Death

Sent to an entity when it dies, with its killer as the actor. In event of a suicide, the killer will be itself.

Hit

Sent to an entity whenever it is hit by an attack, with the attacker as the actor. Additional variables, hit_friendly and hit_total are available to character types to note the strength of the attack, and whetver or not it's considered a friendly effect (eg; heal).

interact

Sent to an object whenever an entity 'interacts' with it. By default the player sends these by pressing E whilst targetting relevant entities.

Import #

This is a signal send to each and every map in turn when you load an old savegme from a previous game version. The purpose of this is to allow the game's maker to update the world state to let older savegames function, or to retroactively correct bugs and oddities.

# refers to the gameversion that is being imported to the next revision.

For example, let's say my savegame was made for revision 1 of the game, and it has since advanced to revision 3 and maintained compatibility with revision 1. Upon loading the game, loading will succeed, and then it will send the signal import 1 to everything, followed by import 2.

Resurrect

Sent whenever an entity respawned at a nearby spawnpoint, this is effectively a player only signal.

Arrived

This signal is exclusive to platforms, it is sent whenever it arrives at once of its destination nodes. Among other things, you can use this to freeze the entity for a little bit when it reaches specific nodes. You also have access to a variable named route_tag which denotes the tag of the platformroute entity you've arrived at.

//For example...

r_script_signal arrived [
	if (>= (indexof "1 3 5 7 9 15" $route_tag) 0) [
		r_select_platform self [
			r_platform_flags (&~ $r_platform_flags $PLAT_ACTIVE)
		]
		r_sleep 500 [
			r_select_platform self [
				r_platform_flags (| $r_platform_flags $PLAT_ACTIVE)
			]
		]
	]
]

References

If signals are the heart and soul of Lamiae's RPG scripting subsystem, then references are the vital organs thereof.

Firstly, every reference is a list of 0 or more objects, and like loading models with multiple animations in the same file, : is used as the deliminator between the reference name and the index, ie reference:id. If you're keeping the index and reference's name in cubescript variables, an easy way to write this is as follows, [@[myref]:@id]. If you desire to know how that works, consult lookups within CubeScript.

Some commands work on the whole list, but these are the exception and limited to the command used to manage the lists. Others will in the absence of an index, assuming you meant to specify :0, aka the first atom in the list.

This list is taken from src/rpggame/rpggame.h
They are reproduced in data/rpg/game.cfg with the prefix, REF_

T_INVALID
T_CHAR
T_ITEM
T_OBSTACLE
T_CONTAINER
T_PLATFORM
T_TRIGGER
T_INV
T_EQUIP (V)
T_MAP
T_VEFFECT (V)
T_AEFFECT (V)

Just about anything in the RPG component can be set to a reference, a wide array of types are supported in a safe manner. Unfortunately some references are rather volatile, these are marked with (V) in the list above.

Volatile types can only persist for a single update at the very most whilst still guaranteeing safety. Keeping these any longer risks that we'll be referencing invalid memory or causing other problems and general havoc. References that are removed out of order are wiped from the reference tables, or rather, set to T_INVALID. If they naturally expire via the game's mechanisms, they are simply wiped at the end of the frame, which is generally a much cheaper solution.

In addition, only non-volatile references are saved to savegames; volatile ones are replaced with empty slots.

The reference system is also stack based, the bottom/global stack has a total of 1024 buckets, whilst additional levels have 64 buckets, optimising Lamiae to work better with more items focused in the Global stack. Though 64 should be plenty for all purposes that involve manipulating the stack before reaching linked list speeds.

As a result of this optimisation, it mimics the behaviour of CubeScript, which is to say that by default, new references are created in the global stack. New temporary references can be declared in the top-level stack by the use of the r_local keyword.

Both r_ref and r_local work identically to the local keyword, which is to say they both take a list of reference names, and then declare these in their respective positions. r_ref behaves somewhat oddly, it searches the whole stack for the given references before creating it in the global scope. For example...

r_stack [
	r_local foo

	//foo was declared above, so it will do nothing there
	//but it will declare bar in the global scope.
	r_ref foo bar
]

Invoking signals temporarily pushes the stack with the references "actor" and "self" put into the new level of the stack. And this introduces the novel idea of shadowing to the system. to demonstrate with a diagram, the references marked with --> will be the ones returned when they're searched for.

Stack 0:
	--> player
	--> curmap
	--> hover
	foo

stack 1:
	actor
	self
	--> foo

stack 2:
	--> actor
	--> self

Reserved References

The following are a list of references you should never set, and this is enforced

  • player This is a reference to the player's instance
  • hover This defines the entity that is currently under your cursor.
  • curmap This defines a reference that points to the current map.
  • self For signals, defines the receiver of the signal
  • actor For signals, defines the sender of the signal
  • talker If you're talking to someone, this is set to point to the person you're communicating with.
  • looter If you're looting something, this points to the chest/person you're interacting with.
  • trader If you're trading with someone, this is set to the object containing the inventory you're dealing with.
  • config This is a temporary reference that is set when an item is being configured. This is strictly to allow interaction with local variables, do not set another reference to this one under any circumstances. Ignoring this advice, the resulting best case scenario is a warning about a missing reference when you save your game for the first time, and load it for the first time.

Managing the reference list

For managing and interacting with reference lists, the following commands are available to you. We'll go over them one by one in the following subsections. These are the general and definitive guide on managing the reference lists.

r_reftype refname
r_clearref
r_ref {refname}
r_local {refname}
r_setref refname newdata
r_swapref first second
r_refexists {refname}
r_matchref first second
r_ref_len refname
r_ref_push refname extra
r_ref_sub first second
r_ref_remove refname index num

There are a great many more commands which will retrieve data and store it in a reference list, such as r_setref_inv which will basically store your whole inventory in a reference list. And there are others like r_loop_ents which will set push a temporary reference of a given name onto the stack and iterate through various values.

r_ref_type

r_ref_type takes the name of a reference with an optional index, and returns the type of the reference stored at the index. Invalid references and indices will return T_INVALID without an error.

r_clearref

r_clearref removes the topmost instances of all given references. The same reference can be passed multiple times to clear multiple instances of it.

r_ref and r_local

r_ref and r_local were covered above. They take a list of reference names and declare them in the appropriate context.

r_ref declares the reference in the global stack if it doesn't exist.

r_local will create all given references in the topmost stack, and if they already exist, they will be emptied.

r_setref

r_setref takes two reference lists, the first is the target reference and the second is the reference from which to source the new data. The presence of the index is important here and the rules are a bit tricky. For the sake of clarity we will refer to these two lists as reflist and alias respectively.

The first case, both lists omit the index, what will happen is reflist will be cleared and its contents will be fully replaced with those from alias. If alias is an invalid reference, then reflist will be cleared.

The second case, both include an index. The reference indicated by the reflist's index will be replaced by the reference in alias's given index. Essentially reflist[refidx] = alias[aidx];. reflist will be filled with T_INVALID references if the index is out of range so that it can fulfil the request. If the given index isn't inrange for alias then the given index is set to null for the reflist.

The third case, reflist omits an index, but alias does not. reflist will be reduced to one element, that element will be what alias and its index pointed to.

The fourth case is the most complex, the reflist has an index, but alias does not. What will happen is, alias in its entirety will replace the reference indicated by reflist's index.

List 1                        List 2
0 --> 0xBABEBABE, T_CHAR      0 --> 0xDEADBEEF, T_INV
1 --> 0xDEADBABE, T_CHAR      1 --> 0xBEEFBEEF, T_ITEM
2 --> 0xCAFEBABE, T_CHAR

if we invoked r_setref List1:1 List2, we will end up with the following

List 1                        List 2
0 --> 0xBABEBABE, T_CHAR      0 --> 0xDEADBEEF, T_INV
1 --> 0xDEADBEEF, T_INV       1 --> 0xBEEFBEEF, T_ITEM
2 --> 0xBEEFBEEF, T_ITEM
3 --> 0xCAFEBABE, T_CHAR

r_swapref

This swaps the contents of two given lists around. Both lists must be valid, but they may be empty.

r_refexists

Takes a list of reference names. Returns 1 if it can find all of them, 0 otherwise.

r_matchref

This checks to see if two given references are equal. They may have indexes, the rules for evaluating them if one list lacks a link gets a bit weird. Note that two invalid lists are considered to not match.

If both references were given separators, the following process takes place...

  1. Lookup both indices,
  2. If both are invalid, return 1
  3. if one is invalid, return 0
  4. otherwise return their equivalence.

If both lack separators, the following takes place...

  1. loop through the first list
  2. If the index is out of range of list two, or the two elements are equivalent, continue, otherwise abort and return 0
  3. return 1 if their lengths are equal
  4. otherwise return 0

Otherwise the following happens

  1. if the index is invalid and the list has a length of 0, return 1
  2. if the index is valid and list has a length of 1, return their equivalence
  3. otherwise return 0

r_ref_len

Returns the amount of items indexed by the reference list. An invalid reference will fallback to 0 without error.

r_ref_push

Appends the second list onto the first list. The second list can specify a separator to only add a single element onto the end of the first.

r_ref_sub

Removes all elements contained in the second which are also in the first list. If the two references are the same reference list, it is wiped.

If a separator is provided for the second list, it will only remove the specified element, even if the two lists are equal.

Both reference lists must be valid and contain at least 1 child, otherwise the process aborts.

r_ref_remove

Takes 3 arguments, the reference list, the index to start at, and the amount of elements to remove. If an invalid index is specified, or of n will go past the end, it will trigger a warning.

At the very least, the very last element will always be removed.

Globals

These refer to global variables, these are named by strings, and their values are also strings. Their length isn't limited, and due to the nature of cubescript, you may store whatever you desire within them.

r_global_new name [value]
r_global_set name [value]
r_global_get name

r_global_new and r_global_set offer the exact same functionality and do the exact same thing. The difference between the two, is that r_global_set warns if the variable doesn't exist already, and r_global_new warns if it does already exist.

Locals

Local variable stacks are currently limited to the non-volatile reference types; worldspace entities, inventory items, and the map itself.

To set and adjust locals, a reference is required. The syntax is otherwise the same as those for globals, excepting that there's no r_local_new, and that r_local_set both creates and sets locals.

r_local_set reference name [value]
r_local_get reference name

These can be used to define various things, for example, what an item in your inventory morphs into when its charges are expired, eg, a health potion turns into an empty bottle.

You can also use it for story purposes with generic NPCs. For example, consider the ever popular Arcanum, if you went to the Tarant Newspaper office and sold them your story of the IFZ Zephyr incident. As a result, each of the generic citizens of Tarant gave you a small batch of gold pieces out of pity, but only once. Locals can be used to achieve an identical effect in a similar situation.

Dialogue

Dialogue follows a very simple system, you directly register nodes in the scripts themselves, and associate two delegates to generate the NPC's response and the player's available responses. The simplest variant takes the following form.

r_script_node "main" [
	result "Hello, I'm an NPC"
] [
	loop i 3 [
		r_response (concat "Hello, I'm the player and this is response number" $i)
	]
]

The nodes themselves are named, and you navigate between them by providing the name as the second argument of r_response. Note that when given the node name, "", this will close dialogue without raising any errors or warnings. It's not an error to set the name of a dialogue node to "", but it will be unreachable.

r_script_node "main" [
	result "Hello, I'm an NPC."
] [
	r_response "I have some questions." questions
	r_response "Goodbye!"
]

r_script_node "questions" [
	at ["I'll try to answer." "Yes?" "What is it?" "hmmm?"] (rnd 4)
] [
	r_response "It's nothing, nevermind."
]

r_response also takes a third argument, this is an optional script, you can use this to kill people, interact with the player's inventory, and if you wanted, you could even target a different NPC to converse with. You have full access to the scripting language, just remember to use talker and player instead of self and actor as the reference names.

r_script_node "main" [
	result "..."
] [
	r_response "I think you should kill yourself." "killyourself"
]

r_script_node "killyourself" [
	result "I agree, I think I should kill myself."
] [
	r_response "Do eet!" "" [r_kill talker]
]

Command Reference