Spawning through script

From Mod Wiki

Jump to: navigation, search

To avoid repetition, study the article on quest creation. Here we will merely tie everything together and learn to spawn objects from a script.

A small aside:

Why is it better to spawn from a script, not from xrSpawner? The xrSpawner application, with all its advantages, has one drawback — it uses the file all.spawn, which leads to:

  • Impossibility of combining two mods using such spawning
  • Necessity of starting the game over each time

The situation when spawning with a script is such: in the overwhelming majority of cases, previously saved games will work, which is good news. :)

Contents


Theory

There is a single script function responsible for spawning objects:

alife():create(section, position, levelvertex, gamevertex)
Strictly speaking, there are two: create and create_ammo, but the differences between them are minimal. Imp 22:45, 23 июля 2007 (EEST)

The first parameter — ltx section describing the object. For instance: "bolt", "med_kit" — are simple sections, simple objects. There are also objects that move between online and offline modes; these are NPCs, monsters, etc. For instance: mil_killer_respawn_2 — spawns a killer sniper.

I guess, position needs no explanation, except one nuance: height is Y, not Z. Position can be created with vector():set(x,y,z), where x, y, and z are coordinates of a point in the level in which we are spawning the object.

Further on it gets complicated and difficult for me to describe.

Lets move from the simple to the complex. Each level contains many objects. All objects consist of polygons, each polygon contains vertices.

This is what we must specified. I do not really understand why; most likely for precise positioning of objects. For example, you can get the vertex nearest to an actor — db.actor:level_vertex_id().

The next parameter is much more interesting; game_vertex_id() is almost the same thing as level_vertex_id(), except that it is a global value! Whereas level_vertex_id() is calculated for the level, game_vertex_id() is for the whole game, and is necessary to indicate which map to spawn the object on (I could not come up with a more sensible explanation).

Correspondingly, to spawn an object on a different map, it is enough to set game_vertex in the fourth parameter. For example:

db.actor:game_vertex_id()

And so, to spawn, for instance, a blot under an actor's feet, we write:

alife():create("bolt", db.actor:position(), 1, db.actor:game_vertex_id())

Why 1, not level_vertex_id()? We checked and found not significant difference which level_vertex_id(), although in some cases a valid vertex is required; without it an object might spawn somewhere other than planned... But in most cases it is ok to specify 1. (Ignoring level_vertex_id may cause spawned objects to fall through the ground.) Now, game_vertex_id() determines everything — it specifies which the level the object will be spawned on, and so is necessary. Theoretically, one could find one game_vertex_id() for each level and use them in scripts. In reality, game_vertex_id() shows which fragment of the map will be used (the whole map is divided into segments numbered across all levels, and game_vertex_id() chooses the right one), so incorrect usage is dangerous. Still you could use level_vertex_id() as well:

alife():create("bolt", db.actor:position(), db.actor:level_vertex_id(), db.actor:game_vertex_id())


Other than that, there is one additional parameter — ID of the object. If an ID of an NPC or an actor is given, the object will be spawned in his inventory.

For example (spawn artifact Medusa in an actor's inventory):

alife():create("af_medusa", db.actor:position(), 1, db.actor:game_vertex_id(), db.actor:id())

The spawn function returns a server object, not an NPC, or a monster, or anything else.

A server object lets the newly created NPC or a stash be filled with various items. For example, lets create a Duty member in front of the entrance to Sidorovich and give him a box of ammo:
local a = vector() -- Set variable type
a.x = -243.61	-- X coordinate
a.y = -19.52	-- height Y
a.z = -127.17	-- Z coordinate
 
local obj = alife():create("bar_dolg_respawn_3",a,13193,8,65535)
alife():create_ammo("ammo_9x18_fmj",
	obj:position(),
	obj:level_vertex_id(),
	obj:game_vertex_id(),
	obj:id(),
	20) -- number of bullets
By the way, create_ammo is practically the same thing. The difference is that create_ammo is meant specifically for spawning ammo, and allows creation of half-full boxes. There might be some other differences. It should be noted that the game authors themselves spawn bullets exclusively with create_ammo. User:Imp 22:38, 23 July 2007 (EST)

The minimum set is coordinates, ID, section, while the server objects is usually useful only for the ID, since the ID could be used to get that very server object:

alife():object(id)

It could be used, for example, to leave a marker, but I use it for other goals — spawning complex variables — NPCs.

Now, try to solve the following problem — create a mercenary, change his faction, change his inventory, and make him friendly towards the player.

At a certain moment, the spawned object enters online mode; at that moment the callback net_spawn is fired.

What do we do? Compare the online object's ID with the stored ID!

If they coincide, such as:

if obj:id()==saved_id then ...

It is important that the server object has an ID as a parameter, while the online object returns it from a function. This is important, or you might get in trouble.

And so, we catch our killer by ID.

The rest is simple — call functions for spawning a Gauss with ammo for it in the NPC's inventory (see above), change his faction with a special function, make him friendly.

Why so complicated? Simply, the NPC does not exist in offline mode. There is only an indirect mention of him and, in addition, all of those functions with "NPC" objects, not with server objects.

How to get coordinates

Solution A

Enter the following command in the console:

rs_stats on or rs_stats 1

This turns on display of information on the screen. Next, enter one more command: demo_record 1

Now, "fly" to the factory. We have to choose a location for spawning objects, and this mode is perfect for it. Set the camera in the location of the planned spawn and record the coordinates. I got 115, -6, -16.

To exit demo_record mode, press Esc and enter rs_stats off or rs_stats 0 in the console (remove information display).

Solution B

To run the code in the next example, you would want to call it by key press. Please read Code execution by button first, if you don't know how to bind code to a key.

Another way of getting this same information is to walk to the proper place and run the following script, which will give you the proper coordinates.
function coordinates_to_message()  
	-- Put a message about our location on screen
	local a    = db.actor:position()        -- Our position's coordinates
	local lvid  = db.actor:level_vertex_id() 
	local gvid = db.actor:game_vertex_id() 
	local text = "Position:\\nX= "..a.x.."\\nY= "..a.y.."\\nZ= "..a.z.."\\nlevel_vertex= "..lvid.."\\ngame_vertex_id= "..gvid
	news_manager.send_tip(db.actor, text, nil, nil, 30000)
end
As a result, you need not experiment, and get everything, including level_vertex_id() and game_vertex_id(). Imp 22:38, 23 July 2007 (EEST)

Solution C

Another possibility is to write the info into the console instead of putting it on the screen. This way you will store it inside the log, after you quit the game. Again, please read Code execution by button first, if you don't know how to bind code to a key.

function coordinates_to_console_and_log()
	-- Put a message about our location into the console and log file
	local a    = db.actor:position()        -- Our position's coordinates
	local lvid  = db.actor:level_vertex_id() 
	local gvid = db.actor:game_vertex_id() 
	local text = level.name().."#XYZ:"..tostring(a.x)..","..tostring(a.y)..","..tostring(a.z).."#Lvid:"..tostring(lvid).."#Gvid:"..tostring(gvid)
	get_console():execute(text)
end

Tools that do the work

There are some tools that help you saving coordinates, so you don't have to fiddle with scripting:

Practice (part 1)

Now, lets make a quest.

Problem:

After speaking with Sidorovich, spawn a zombie at the plant in the first location.

Implementation:

1. First, run the game and collect coordinates as described above.

Exit the game, go to the folder in which the game is installed, create the gamedata folder (assuming the mod is created over a clean game, with no other mods, and have the unpacked resourced in some folder, such as gamedata source).

Create folder config in gamedata, in it — folder creatures. Copy file m_zombie.ltx from the original folder and open it for editing.

There are 5 models of zombies in the game files, in files: files zombi_1.ogf, zombi_1_ghost.ogf, zombi_2.ogf, zombi_trup.ogf, zombi_trup_2.ogf.

Lets return them all into the game. :)

We already have sections: [zombie_weak]:m_zombie_e, [zombie_normal]:m_zombie_e, [zombie_strong]:m_zombie_e и [zombie_immortal]:zombie_strong.

The last two types use the same model zombi_trup.ogf, hm... not right, fix this. The section should look like this:

[zombie_immortal]:zombie_strong
$spawn          = "monsters\zombies\zombie_immortal"
visual          = monsters\zombi\zombi_trup_2
panic_threshold = 0.0

Add the fifth model.

To achieve this, create the following section at the end of the file:

[zombie_ghost]:zombie_strong

This means that our fifth zombie will inherit all parameters of zombie_strong. We only add a visual appearance.

Writer further:

$spawn = "monsters\zombies\zombie_ghost"
visual = monsters\zombi\zombi_1_ghost

That's it. Save all changes and close the file.

2. Writing the spawn script. Create folder scripts in folder gamedata, in it create a new text document and name it esc_zombie.script.

Third aside:

During the writing of the article the original zombie_stori.script from horror-mod was used. The spawn concept has been carried over practically unaltered, therefore I certainly have no claim to the authorship of this method. :)

So, open our empty file for editing, in the first line declare a variable for storing our zombie:

local zombie_types = {"zombie_weak", "zombie_normal", "zombie_strong", "zombie_immortal", "zombie_ghost"}

Next, write the function:

function spawn_zombies( position, total )
	local zombie_index                       -- zombie type from zombie_types array
	local new_pos, x_offset, z_offset        -- declare variables
	for zombie_index=1, total do             -- spin the loop the number of times given by the variable "total"
		x_offset = math.random(5)        -- random x between 1 and 5
		z_offset = math.random(5)        -- random z between 1 and 5
		new_pos = position               -- transfer the coordinates to a function
		new_pos.x = new_pos.x + x_offset -- add the random x to the x coordinate
		new_pos.z = new_pos.z + z_offset -- add the random z to the z coordinate
		-- below we call the function of random zombie spawn itself for zombie_types[math.random(5)] tied to our coordinates
		alife():create(zombie_types[math.random(5)], new_pos, db.actor:level_vertex_id(), db.actor:game_vertex_id())
	end
end

And lastly:

function zomby_story_1( actor, npc )
-- ten zombies at the plant
	local spawn_point = vector():set( 115, -6, -16 ) -- here we specify the coordinates we got while "flying"
	spawn_zombies( spawn_point, 10 )                 -- here is the call of the previous function itself, giving it the coordinates and the number of objects
end

That's it. Save and close the file.



Continuing the discussion :)

To make sure that the game does not crash after we add a new type of monster, we must add them to the file xr_statistic.script. So, copy this file from the scrips folder of the game into our folder alongside esc_zombie.script and open it for editing.

Add to local killCountProps to monsters the line:

zombie_weak = 1, zombie_normal = 2, zombie_strong = 3

To local sect_alias the line:

zombie_weak = "zombie_weak", zombie_normal  = "zombie_normal", zombie_strong = "zombie_strong"

To monster_classes below the line:

[clsid.zombie_s] = "zombie"

To function getNpcType(npc) we add:

elseif npc:character_community() == "zombie" then
	community = "zombie"

Save changes and close the file.

Everything will work great, until we try to loot a killed zombie. The moment we do this, the game will crash with an error like this:

Expression    : fatal error
Function      : CInifile::r_string
File          : D:\xray-svn\xrCore\Xr_ini.cpp
Line          : 351
Description   : <no expression>
Arguments     : Can't find variable icon in [zombie_weak]

All correct — the game does not know which icon to show for the zombie. Monster icons are stored in the file ui_npc_monster.dds. Here we have to alternatives:

  • If you are friends with Photoshop, edit this file (paint and add icons). (Tutorial on editing Stalker UI.dds files in Wiki)
  • Take an existing one from any mod, with the author's permission, of course. Right now we will skip this step and give our zombie the controller's icon.

Return to file m_zombie.ltx and into the middle of the [m_zombie_e]:monster_base section add the parameter:

icon = ui_npc_monster_kontroler

This is it. There will be no more crashes.

3. The current article won't go into detail regarding creation of new dialogues. In the beginning of the article I mentioned a source in which you can find exhaustive information regarding dialogue creation. For instance, the article on dialogue creation (Russian) by BAC9-FLCL.

We merely need to check correctness of the spawn, so I will give you an example only the modified dialog from file dialogs_escape.xml:

<dialog id="escape_trader_talk_info">
	<phrase id="999">
		<text>escape_trader_talk_info_999</text>
		<next>7770</next>
		<next>9991</next>
		<next>9992</next>
		<next>9993</next>
		<next>9994</next>
		<next>9995</next>
		<next>9996</next>
	</phrase>
	<phrase id="9992">
		<text>escape_trader_talk_info_9992</text>
		<next>99922</next>
	</phrase>
	<phrase id="99922">
		<text>escape_trader_talk_info_99922</text>
		<next>9996</next>
		<next>9995</next>
	</phrase>
	<phrase id="9993">
		<text>escape_trader_talk_info_9993</text>
		<next>99933</next>
	</phrase>
	<phrase id="9995">
		<text>escape_trader_talk_info_9995</text>
	</phrase>
	<phrase id="3121">
		<text>escape_trader_talk_info_3121</text>
		<next>9996</next>
		<next>9995</next>
	</phrase>
	<phrase id="3131">
		<text>escape_trader_talk_info_3131</text>
		<next>9996</next>
		<next>9995</next>
	</phrase>
	<phrase id="41">
		<text>escape_trader_talk_info_41</text>
		<next>9996</next>
		<next>9995</next>
	</phrase>
	<!------Our dialog: begin------->
	<phrase id="7770">
		<text>escape_trader_talk_info_7770</text>
		<next>7771</next>
	</phrase>
	<phrase id="7771">
		<text>escape_trader_talk_info_7771</text>
		<next>7772</next>
		<next>7773</next>
	</phrase>
	<phrase id="7772">
		<text>escape_trader_talk_info_7772</text>
		<next>7777</next>
	</phrase>
	<phrase id="7773">
		<text>escape_trader_talk_info_7773</text>
		<next>7779</next>
	</phrase>
	<phrase id="7779">
		<text>escape_trader_talk_info_7779</text>
		<next>9996</next>
		<next>9995</next>
	</phrase>
	<phrase id="7777">
		<text>escape_trader_talk_info_7777</text>
		<action>esc_zombie.zombie_story_1</action>
		<next>9996</next>
		<next>9995</next>
	</phrase>
	<!------Our dialog: end------->
	<phrase id="51">
		<text>escape_trader_talk_info_51</text>
		<next>9996</next>
		<next>9995</next>
	</phrase>
</dialog>

Also,k the file stable_dialogs_escape.xml linked to it. At the very start of the file write the following:

<string id="escape_trader_talk_info_7770">
	<text>Происшествий никаких не было?</text>
</string>
<string id="escape_trader_talk_info_7771">
	<text>Да знаешь... Вроде как тихо все у нас. Хотя, вот, вспомнил! Говорили мне 
на днях, что на фабрике, ну, там, где бандюки околачиваются постоянно, видели какиих-то то ли 
людей, то ли призраков... Мало ли что спьяну почудится - я и сказал этим паникерам, мол, 
закусывать надо! Хех, блин, алкаши...</text>
</string>
<string id="escape_trader_talk_info_7772">
	<text>Дык мне по любому мимо фабрики топать - заодно и посмотрю на этих 
"людей-призраков".</text>
</string>
<string id="escape_trader_talk_info_7773">
	<text>Да я как-то не собирался в ту сторону...</text>
</string>
<string id="escape_trader_talk_info_7779">
	<text>Ну, смотри сам, все равно будь осторожен.</text>
</string>
<string id="escape_trader_talk_info_7777">
	<text>Ага. Сходи, проветрись. Потом зайдешь, расскажешь, что там и как.</text>
</string>
<string id="esc_bridge_soldiers_start_11">
	<text>Здесь проход воспрещён, сталкер.</text>
</string>

This is all. You can start the game, go to Cordon, following the speech with Sidorovich, depending on the choice made, run towards the Plant and ... see for yourself.

the files for this example
Spawn Lib

Homework — return the sixth type of zombie into the game. :)

To be continued...

Practice (part 2)

4. Today we complete the zombie — add their descriptions to the encyclopedia, add icons, work out the "homework". :) I think that if you study this article carefully, you will be able restore any character that has not made it into the final build of the game yourself with script functions. If you have enough time and patience, you can make something like the "night of the living dead" :) — a series of quests connected with the main storyline. So, the "homework" — adding the sixth zombie to the game:

  • Create in folder gamedata folder meshes and in it folder monsters, and there folder zombi. In the meshes folder models for characters, objects, and environment objects from the game are kept. As I said above, there are 5 zombie models in the game, but 6 textures. The numbering is also a little confusing — 1, 2, 4... where is the third? Must have ran away. :)
  • Copy from the folder with the original game files into the one we created file zombi_2.ogf and rename it into zombi_3.ogf. Open fhe file of our model with your favorite hex editor (I use Binary Editor 1.00). Since this article is targeted not only at advanced users, but also beginners that want to make something, I won't bore you with addresses, byte notation, etc, but will simply show you what to edit :)

Model file before editing

Model file after editing

You can see on these screens that we have simply edited the texture path for this model. That's it. Without using a 3D editor and spending a lot of time, we get a brand new model (or so it would seem). :)

5. Now we add our zombie to all files we created earlier. To the file m_zombie.ltx we add at the very end the section:

[zombie_old]:zombie_normal
$spawn = "monsters\zombies\zombie_old"
visual = monsters\zombi\zombi_3

In the file esc_zombie.script we change the array in the first line:

local zombie_types = {"zombie_weak", "zombie_normal", "zombie_strong", "zombie_immortal", 
"zombie_old", "zombie_ghost"}

In function spawn_zombies we modify the spawning line:

alife():create(zombie_types[math.random(6)],new_pos,
db.actor:level_vertex_id(),db.actor:game_vertex_id())

In function zombie_story_1 change the number of objects to a multiple of 6 (not necessary):

spawn_zombies( spawn_point, 12 )

That's it. Save and close.

6. Copy into the folder gamedata\config\gameplay\ the file encyclopedia_mutants.xml, add the description of the zombie to the encyclopedia:

<!-------------------------------- Zombieg ----------------------------->
<article id="mutant_zombieg_general" name="Zombieg" group="Mutants">
	<texture>ui_npc_monster_zombieg</texture>
	<text>enc_mutant_zombieg_general</text>
</article>

To the linked file string_table_enc_mutants.xml in folder gamedata\config\text\rus\ add:

<string id="Zombie">
	<text>Зомби, гражданский</text>
</string>
<string id="Zombieg">
	<text>Зомби-призрак, гражданский</text>
</string>
<string id="enc_mutant_zombieg_general">
	<text>Зомби-привидение отличается от обычного зомби лишь тем, что 
воздействие Выжигателя мозгов полностью разрушило не только структуру личности, но и 
тело, поэтому зомби-призрак несколько более живуч по сравнению с обычным зомби.</text>
</string>
<string id="enc_mutant_zombie_general">
	<text>Воздействие Выжигателя мозгов полностью разрушает структуру личности, 
оставляя только телесную оболочку.\n\n Побродив немного по Зоне, лишённые разума тела 
начинают превращаться в настоящих зомби. Из рефлексов у них остаются лишь самые примитивные, 
оружие и экипировка скоро приходят в негодность. В результате зомби становятся собой ни чем 
иным, как медлительными полутрупами, которых наличествуют лишь два эффективных раздражителя: 
еда и сон. Зомби совершенно неразборчивы в выборе пищи и питья, поэтому их тела буквально 
пропитаны радиацией и токсинами. Как правило, эти существа бесцельно бродят по Зоне или, 
словно трупы, валяются внутри заброшенных построек. Однако, лишь только зомби почует близкое 
присутствие живого человека, он сразу же пытается атаковать. Умудрённые опытом сталкеры 
стараются обходить эти неуклюжие опустошённые оболочки.</text>
</string>

Copy into the same place the file stable_stable_caption.xml and change 3 lines:

<string id="zombie_normal">
	<text>зомбированный, гражданский</text>
</string>
<string id="zombie_strong">
	<text>зомби-призрак, гражданский</text>
</string>
<string id="zombie_weak">
	<text>зомби, гражданский</text>
</string>

Save changes and close.

7. And lastly — add the icons. I used the ready-made file containing icons for zombies and other "reincarnated" monsters (thanks to Fr3nzy). Simply copy the file ui_npc_monster.dds from the archive into the folder gamedata\textures\ui\, and the file ui_npc_monster.xml — into the folder gamedata\config\ui\. If you want to create your own, read the tutorial on modifying textures (Russian).

In short, here is what the file ui_npc_monster.xml describes: it sets icon coordinates, located in the file ui_npc_monster.dds, for each monster in the game.

Last stroke. Open the file m_zombie.ltx and in the first section replace the line

icon = ui_npc_monster_kontroler

by:

icon = ui_npc_monster_zombie

In section [zombie_ghost] add the line:

icon = ui_npc_monster_zombieg

Save all changes. This is it.

Here are ready files. Good luck and thank you for your attention. :)

Spawning NPC

This part is written by user Arhet and is modeled after the way the NPC faction "Sin" is implemented in the SRP Mod.

Use files:

  • gamedata\config\gameplay\character_desc_escape.xml
  • gamedata\config\gameplay\npc_profile.xml
  • gamedata\config\creatures\spawn_sections.ltx

Begin with character_desc_escape.xml. I won't describe the meaning of each line, since all of this has been done before me.

Here we create a new character:

<specific_character id="esc_dark_stalker_1" team_default = "1">
	<name>GENERATE_NAME_bandit</name>
	<icon>ui_npc_dark_1</icon>
	<map_icon x="0" y="0"></map_icon>
	<bio>sim_stalker_novice_bio</bio>
 
	<class>esc_dark_stalker_01</class> <!-- this uses our new class -->
	<community>dark_stalker</community> <terrain_sect>stalker_terrain</terrain_sect>
	<money min="200" max="600" infinitive="0"></money>
 
	<rank>198</rank>
	<reputation>-24</reputation>
 
	<visual>actors\dark_stalker\dark_stalker_1</visual>
	<snd_config>characters_voice\human_01\monolith\</snd_config>
	<crouch_type>-1</crouch_type>
	<panic_treshold>0</panic_treshold>
	<supplies>
		[spawn] \n
		wpn_dark_colt1911 \n
		ammo_11.43x23_hydro \n
		#include "gameplay\character_items.xml" \n
		#include "gameplay\character_drugs.xml" \n
		#include "gameplay\character_food.xml"
	</supplies>
 
	#include "gameplay\character_criticals_3.xml"
 
	<start_dialog>dm_hello_dialog</start_dialog>
	<actor_dialog>dm_cool_info_dialog</actor_dialog>
</specific_character>

We add our code after some </specific_character>

Now, go to npc_profile.xml and enter:

<character id="esc_dark_stalker_1">
	<class>esc_dark_stalker_01</class>
	<specific_character>esc_dark_stalker_1</specific_character>
</character>

Now, lets go to spawn_section.ltx. The script will "grab" the NPC from this very file. Enter there:

[esc_dark_stalker_1]:stalker ; name of the section for the script
$spawn            = "respawn\esc_dark_stalker_1"
character_profile = esc_dark_stalker_1 ; link to the description of our NPC
spec_rank         = regular ; special rank
community         = dark_stalker ; faction

Now, take any NPC spawn script, substitute the name of the section from spawn_section.ltx and voilà!

Except, the NPC will have default logic (it will wonder about aimlessly). We will get to that later.

Personal tools
In other languages