User:ArthurAutomaton/sandbox: Difference between revisions

From Valve Developer Community
Jump to navigation Jump to search
(→‎Sandbox: dota hammer entities)
Line 395: Line 395:
  end
  end
</source>
</source>
== Dota Hammer entities ==
Extracted from <code>/path/to/Steam/SteamApps/common/dota 2 beta/dota_ugc/game/dota/tools/help/fgd/dota.txt</code>:
;npc_dota_spawner
:Spawns NPCs based on scripts.
;npc_dota_scripted_spawner
:Spawns NPCs based on scripts for tutorials / single player / co-op.
;dota_color_correction
:An entity to control the color correction in the map.
;dota_item_rune_spawner
:A marker for where runes spawn.
;dota_minimap_boundary
:Used by the console command dota_minimap_create to define the min/max coordinates for taking minimap images. Two are required per map in opposite corners. The z difference between the two defines the image depth.
;ent_dota_dire_candybucket
:(used in Diretide event) Spawns a Dire candy bucket.
;ent_dota_fountain
:The Fountain
;ent_dota_shop
:Creates a Dota item shop.
;ent_dota_tree
:Creates a Dota tree.  Trees have collision, block fog of war visibility, and can be destroyed.  Note: it's far faster to place trees via the Tile Editor tool.
;npc_dota_barracks
:Creates a barracks, which spawns creeps at regular intervals and can be destroyed by enemy teams.
;npc_dota_building
:Creates a simple building.
;npc_dota_fort
:Creates an ancient. In standard Dota2 PvP, the win condition is to destroy the enemy team's ancient.
;npc_dota_holdout_tower_heavyslow
:(used in Frostivus event) Creates a heavily-damaging, slowly-attacking tower.  This can also be edited in the npc_units.txt file.
;npc_dota_holdout_tower_lightfast
:(used in Frostivus event) Creates a lightly-damaging, fast-attacking tower.  This can also be edited in the npc_units.txt file.
;npc_dota_neutral_spawner
:Spawns neutral units.
;npc_dota_roshan_spawner
:Spawns Roshan, the big boss in the standard Dota2 PvP map.
;npc_dota_spawner_bad_bot
:Spawns Dire creeps in the bottom lane of the standard Dota2 PvP map.
;npc_dota_spawner_bad_mid
:Spawns Dire creeps in the middle lane of the standard Dota2 PvP map.
;npc_dota_spawner_bad_top
:Spawns Dire creeps in the top lane of the standard Dota2 PvP map.
;npc_dota_spawner_good_bot
:Spawns Radiant creeps in the bottom lane of the standard Dota2 PvP map.
;npc_dota_spawner_good_mid
:Spawns Radiant creeps in the middle lane of the standard Dota2 PvP map.
;npc_dota_spawner_good_top
:Spawns Radiant creeps in the top lane of the standard Dota2 PvP map.
;npc_dota_tower
:Creates a Dota tower that attacks enemy units, provides vision of invisible enemies, and allows allies to teleport to it.
;ent_dota_game_events
:Fires outputs based on predefined game events.
;dota_displacement_visibility
:Entity that can hide a displacement
;ent_sugar_rush
:Sugar Rush spot for Roshan.
;ent_dota_radiant_candybucket
:(used in Diretide event) Spawns a Radiant candy bucket.
;ent_dota_lightinfo
:Controls localized lighting settings in a radius.  This can override global settings.
;ent_fow_blocker_node
:Blocks fog of war along a line to its target.
;ent_fow_revealer
:Reveals fog of war in a radius.
;info_player_start_goodguys
:Spawn point for Radiant heroes.
;info_player_start_badguys
:Spawn point for Dire heroes.
;env_occluder
:A test entity for radius occluding for the fog of war system.
;env_line_occluder
:A test entity for line occluding for the fog of war system.
;env_viewer
:A test entity for viewing for the fog of war system.
;dota_prop_customtexture
:Custom texture prop.
;dota_world_particle_system
:Dota world particle system.
;prop_dynamic_clientside
:Client-side dynamic prop.
;prop_player_cosmetic
:Cosmetic player prop.
;ambient_creatures
:Client-side dynamic prop.
;ambient_creatures_zone
:Client-side func_brush.
;world_bounds
:Defines camera constraint coordinates.
;dota_building
:dota_building
;npc_dota_holdout_tower_reducespeed
:(used in Frostivus event) Creates a tower that reduces enemy movespeed.  This can also be edited in the npc_units.txt file.
;ent_dota_halloffame
:The Hall of Fame.
;info_courier_spawn_radiant
:Spawns a Radiant team courier.
;info_courier_spawn_dire
:Spawns a Dire team courier.
;trigger_shop
:Region that defines the shop radius.
;trigger_no_wards
:Wards disallowed here.
;trigger_boss_attackable
:Boss (Roshan) is attackable from here.
;trigger_hero
:A trigger that's fired when a hero touches it.
;tutorial_npc_blocker
:The start or endpoint of an NPC blocking line.
;info_roquelaire_perch
:(used in Dota2 PvP tutorial) A perch point for Roquelaire to sit on.
;env_global_light
:Defines global light settings.
;env_deferred_light
:Deferred light entity.
;env_deferred_spot_light
:Deferred spot light.

Revision as of 06:04, 10 September 2014

Sandbox

Some notes to myself about modding DotA 2.

Creating a unit that's controllable by a specific player

This code creates a mud golem at (0, 0, 0) on the Radiant team and makes it controllable by player 0:

 local unit_team = DOTA_TEAM_GOODGUYS
 local unit_name = "npc_dota_neutral_mud_golem"
 local player = PlayerResource:GetPlayer(0)
 local point = Vector(0, 0, 0)
 
 local unit = CreateUnitByName(unit_name, point, true, player, player:GetAssignedHero(), unit_team)
 unit:SetControllableByPlayer(player:GetPlayerID(), true)


Relevant links:

Triggers and shared mutable state

Problem

You have two triggers, trigger_get and trigger_set. When trigger_set is triggered, you want to save some information in a certain global variable. When trigger_get is triggered, you should be able to access that information.

An approach that doesn't work

Warning.pngWarning:Here is a common approach that DOES NOT WORK

1. Create scripts/vscripts/trigger.lua with the following content:

 FOO = 0 -- we want to save information in this global variable
 
 function SetSharedVar ()
    FOO = 1
    print("SetSharedVar called; FOO = " .. FOO)
 end
 
 function GetSharedVar ()
    print("GetSharedVar called; FOO = " .. FOO)
 end

2. In Hammer, edit the properties of trigger_get as follows:

  • Set Entity Scripts = trigger.lua.
  • Add an output like this:
My Output Target Entity Target Input Parameter Delay Only Once
Io11.png OnStartTouch trigger_get CallScriptFunction GetSharedVar 0.00 No

3. In Hammer, edit the properties of trigger_set as follows:

  • Set Entity Scripts = trigger.lua.
  • Add an output like this:
My Output Target Entity Target Input Parameter Delay Only Once
Io11.png OnStartTouch trigger_set CallScriptFunction SetSharedVar 0.00 No

The expected outcome: When a unit steps on trigger_set and then afterwards steps on trigger_get, we expect to see the following output in the console:

SetSharedVar called; FOO = 1
GetSharedVar called; FOO = 1

What really happens: What we actually see in the console is this:

SetSharedVar called; FOO = 1
GetSharedVar called; FOO = 0

This shows that the assignment FOO = 1 made by SetSharedVar is NOT visible to trigger_get.

My guess about why this happens: When you "bind" some Lua code to an entity by setting its Entity Scripts property, that code is "private" to the entity: The entity initially has ITS OWN "blank" Lua environment (with only the library functions and the game API visible), which no other entity can access. When the map is loaded, the game engine populates this environment by running the Lua file specified by the Entity Scripts property. In our case, we set Entity Scripts = trigger.lua on both triggers, but they both get THEIR OWN COPIES of the global variables in trigger.lua. So the assignment FOO = 1 made by trigger_set is not visible to trigger_get since it just changes trigger_set's copy of FOO.

An approach that works

Follow steps 1-3 above but replace Step 2 with this:

2'. In Hammer, edit the properties of trigger_get as follows:

  • DO NOT SET THE Entity Scripts PROPERTY
  • Add an output like this (note that we now use trigger_set as the target):
My Output Target Entity Target Input Parameter Delay Only Once
Io11.png OnStartTouch trigger_set CallScriptFunction GetSharedVar 0.00 No

With this solution we get the expected output in the console. Why? If the guess above is correct, it's because we're now using trigger_set as the target, hence GetSharedVar now accesses trigger_set's copy of FOO.

Holdout stuff

  • For the bosses they use the vscripts key (in npc_units_custom.txt) to set the AI file that should be used.
    • In the AI files the global variable thisEntity (which is not initialized anywhere in the files) seems to refer to the unit itself.
    • Also it seems that if there's a function called Spawn in the file, then it will be called (with a certain table as argument) when the unit is spawned.
    • How does Ogre Magi get its AI file hooked up?

Passing an argument to a thinker

Problem

You want to start a thinker at a certain time during the game, and you want this thinker to have access to some information that's only available at this time.

Solution

Take advantage of the fact that functions in Lua can access variables of their enclosing functions:

 -- addon_game_mode.lua

 function Activate ()
    ListenToGameEvent("entity_killed", ExactDelayedRevenge, nil)
 end

 -- Three seconds after an entity has been killed its attacker will be killed
 function ExactDelayedRevenge (keys)
    local attacker = EntIndexToHScript(keys.entindex_attacker)
    GameRules:GetGameModeEntity():SetThink(function ()
          -- We can access attacker since it's a variable of our enclosing function
          if attacker and attacker:IsAlive() then
             attacker:ForceKill(false)
          end
    end, 3)
 end

Almost interactive development

Tip.pngTip:Typing "script_reload_code test" in the console will cause the Lua code in scripts/vscripts/test.lua to be executed.

Changing properties of a hero on their first spawn

Problem

You need to initialize a hero through Lua when it spawns for the first time (which is normally when it gets selected by a player through the hero selection screen).

Solution

Register a listener on the "npc_spawned" event. The listener should check whether the spawned NPC is a real hero, in which case it should initialize the NPC and mark it as initialized (so that it doesn't get re-initialized the next time it spawns):

 -- addon_game_mode.lua

 local INIT_MARK = DoUniqueString("INIT") -- For marking a hero as initialized

 function Activate ()
    ListenToGameEvent("npc_spawned", OnNpcSpawned, nil)
 end

 function OnNpcSpawned (keys)
    local npc = EntIndexToHScript(keys.entindex)
    if npc and npc:IsRealHero() and not npc[INIT_MARK] then
       -- Initialize hero here, e.g.:
       npc:GetAbilityByIndex(0):SetLevel(3)
       npc[INIT_MARK] = true
    end
 end

Discussion

We can improve the above solution in at least two ways:

  1. The variable INIT_MARK is currently visible to the whole file. When there's a lot of code in the file, this can make it hard to figure out where the variable is used.
  2. We can make the OnNpcSpawned function easier to extend. Right now, if we want to use the function to initialize non-heroes, we have to change both the test and the body of the if statement.

We can solve both problems by introducing a "factory" function that takes an initialization function as argument:

 -- addon_game_mode.lua

 function Activate ()
    ListenToGameEvent("npc_spawned", MakeNpcInitializer(InitializeHero), nil)
 end

 -- Factory function for making listeners
 function MakeNpcInitializer (initFn)
    local initMark = DoUniqueString("INIT") -- private to this factory function
    return function (keys)
       local npc = EntIndexToHScript(keys.entindex)
       if npc and not npc[initMark] then
          initFn(npc)
          npc[initMark] = true
       end
    end
 end

 function InitializeHero (npc)
    if npc:IsRealHero() then
       -- Initialize hero here, e.g.:
       npc:GetAbilityByIndex(0):SetLevel(3)
    end
 end

Cancelling a thinker

Problem

You need to cancel a thinker that you have started.

Solution

Let the thinker return nil; this will cause it to stop:

 -- some_trigger.lua

 local IS_CANCELLED = false
 local THINK_INTERVAL = 1

 function OnStartTouch ()
    GameRules:GetGameModeEntity():SetThink(function ()
          if IS_CANCELLED then
             return nil -- Returning nil stops the thinker
          else
             print("not cancelled yet; proceeding")
             return THINK_INTERVAL
          end
    end)
 end

 function OnEndTouch ()
    IS_CANCELLED = true
 end

Discussion

The above example is quite crude. Provided that some_trigger.lua is set as the entity script of a trigger with the appropriate outputs, it will work like this: Every time a unit steps onto the trigger, a thinker will be started that prints every second. When a unit steps off the trigger, all these thinkers will stop. We can refine this as follows:

 -- some_trigger.lua

 local CANCEL_MARK = DoUniqueString("CANCEL")
 local THINK_INTERVAL = 1

 function OnStartTouch (keys)
    local activator = keys.activator
    activator[CANCEL_MARK] = nil
    activator:SetThink(function ()
          if activator and not activator[CANCEL_MARK] then
             -- Not cancelled yet; do some stuff here
             print(activator:GetName())
             return THINK_INTERVAL
          end
    end)
 end

 function OnEndTouch (keys)
    keys.activator[CANCEL_MARK] = true
 end

This code has the following effect: Every time a unit steps onto the trigger, a thinker starts that prints the unit's name every second. When a unit steps off the trigger, his thinker is cancelled.

Listening to game events

This is a tutorial for beginners. If you see something that can be improved, please improve it!

Introduction

This tutorial is about the API function ListenToGameEvent. If you call ListenToGameEvent with an event name and a function, the game will call that function every time the event happens.

Example 1

Every time a player levels up, we show a message to all players:

 -- scripts/vscripts/addon_game_mode.lua

 function LevelUpMessage (eventInfo)
     Say(nil, "Someone just leveled up!", false)
 end

 function Activate ()
     ListenToGameEvent("dota_player_gained_level", LevelUpMessage, nil)
 end

We have two functions here:

  • LevelUpMessage: Here we use the API function Say to show a message to all players.
  • Activate: The game calls this function when it loads the addon. We want the game to call LevelUpMessage every time a player levels up. To do this we call the API function ListenToGameEvent with "dota_player_gained_level" and LevelUpMessage as arguments.

Example 2

When a player reaches level 6, we show a message to all players:

 -- scripts/vscripts/addon_game_mode.lua

 function Level6Message (eventInfo)
     if eventInfo.level == 6 then
         Say(nil, "Someone just reached level 6", false)
     end
 end

 function Activate ()
     ListenToGameEvent("dota_player_gained_level", Level6Message, nil)
 end

Note: In Level6Message we use the argument, eventInfo, to get the level that the player has reached.

The function ListenToGameEvent

Here is the signature of ListenToGameEvent:

int ListenToGameEvent(string eventName, handle functionToCall, handle context)

Parameters

eventName
The name of the event that you want to listen to. See the page Built-In Engine Events for a complete list.
functionToCall
The function that you want the game to call when the event happens.
context
Here you can use either nil or an object (for example, a table or a function). If you use an object here, then it will be passed as the first argument to your function when the event happens.

Event details

By calling ListenToGameEvent with an event name and a function, you tell the game to call that function every time the event happens. When the game calls the function, it passes a table as an argument to it. This table contains details of the event. On the page Built-In Engine Events you can see which keys the table has. For example, on this page you can see that the table for "dota_player_gained_level" has a key called level (cf. Example 2 above).

Warning.pngWarning:Because Valve sometimes change the events, the list Built-In Engine Events is not always accurate. You can use the pairs function to see which keys a table has:
 for key,val in pairs(tbl) do
     print(key, val)
 end
(Replace tbl with the table you want to inspect.)

Your listener function

If your function is defined like this:

 function MyFunction (arg)
     -- some code here
 end

then call ListenToGameEvent like this:

 ListenToGameEvent("some_event_name", MyFunction, nil)

But what if your function is defined like this?

 function MyClass:MyFunction (arg)
     -- some code here
 end

Remember: This is a short way of writing

 function MyClass.MyFunction (self, arg)
     -- some code here
 end

So your function actually takes two arguments. In this situation, call ListenToGameEvent like this:

ListenToGameEvent("some_event_name", MyClass.MyFunction, ?)

OR like this:

ListenToGameEvent("some_event_name", Dynamic_Wrap(MyClass, "MyFunction"), ?)

Replace ? with the object that you want to use as self in your function. Here is an example:

Example 3

When an NPC spawns, we print the total number of times this has happened:

 -- scripts/vscripts/addon_game_mode.lua

 if MyClass == nil then
     MyClass = class({})
 end

 function MyClass:InitGameMode ()
     self.numSpawned = 0
     ListenToGameEvent("npc_spawned", MyClass.MyFunction, self)
 end

 function MyClass:MyFunction ()
     self.numSpawned = self.numSpawned + 1
     print("Number of times an NPC has spawned: " .. self.numSpawned)
 end

 function Activate ()
     GameRules.MyAddon = MyClass()
     GameRules.MyAddon:InitGameMode()
 end

Dota Hammer entities

Extracted from /path/to/Steam/SteamApps/common/dota 2 beta/dota_ugc/game/dota/tools/help/fgd/dota.txt:

npc_dota_spawner
Spawns NPCs based on scripts.
npc_dota_scripted_spawner
Spawns NPCs based on scripts for tutorials / single player / co-op.
dota_color_correction
An entity to control the color correction in the map.
dota_item_rune_spawner
A marker for where runes spawn.
dota_minimap_boundary
Used by the console command dota_minimap_create to define the min/max coordinates for taking minimap images. Two are required per map in opposite corners. The z difference between the two defines the image depth.
ent_dota_dire_candybucket
(used in Diretide event) Spawns a Dire candy bucket.
ent_dota_fountain
The Fountain
ent_dota_shop
Creates a Dota item shop.
ent_dota_tree
Creates a Dota tree. Trees have collision, block fog of war visibility, and can be destroyed. Note: it's far faster to place trees via the Tile Editor tool.
npc_dota_barracks
Creates a barracks, which spawns creeps at regular intervals and can be destroyed by enemy teams.
npc_dota_building
Creates a simple building.
npc_dota_fort
Creates an ancient. In standard Dota2 PvP, the win condition is to destroy the enemy team's ancient.
npc_dota_holdout_tower_heavyslow
(used in Frostivus event) Creates a heavily-damaging, slowly-attacking tower. This can also be edited in the npc_units.txt file.
npc_dota_holdout_tower_lightfast
(used in Frostivus event) Creates a lightly-damaging, fast-attacking tower. This can also be edited in the npc_units.txt file.
npc_dota_neutral_spawner
Spawns neutral units.
npc_dota_roshan_spawner
Spawns Roshan, the big boss in the standard Dota2 PvP map.
npc_dota_spawner_bad_bot
Spawns Dire creeps in the bottom lane of the standard Dota2 PvP map.
npc_dota_spawner_bad_mid
Spawns Dire creeps in the middle lane of the standard Dota2 PvP map.
npc_dota_spawner_bad_top
Spawns Dire creeps in the top lane of the standard Dota2 PvP map.
npc_dota_spawner_good_bot
Spawns Radiant creeps in the bottom lane of the standard Dota2 PvP map.
npc_dota_spawner_good_mid
Spawns Radiant creeps in the middle lane of the standard Dota2 PvP map.
npc_dota_spawner_good_top
Spawns Radiant creeps in the top lane of the standard Dota2 PvP map.
npc_dota_tower
Creates a Dota tower that attacks enemy units, provides vision of invisible enemies, and allows allies to teleport to it.
ent_dota_game_events
Fires outputs based on predefined game events.
dota_displacement_visibility
Entity that can hide a displacement
ent_sugar_rush
Sugar Rush spot for Roshan.
ent_dota_radiant_candybucket
(used in Diretide event) Spawns a Radiant candy bucket.
ent_dota_lightinfo
Controls localized lighting settings in a radius. This can override global settings.
ent_fow_blocker_node
Blocks fog of war along a line to its target.
ent_fow_revealer
Reveals fog of war in a radius.
info_player_start_goodguys
Spawn point for Radiant heroes.
info_player_start_badguys
Spawn point for Dire heroes.
env_occluder
A test entity for radius occluding for the fog of war system.
env_line_occluder
A test entity for line occluding for the fog of war system.
env_viewer
A test entity for viewing for the fog of war system.
dota_prop_customtexture
Custom texture prop.
dota_world_particle_system
Dota world particle system.
prop_dynamic_clientside
Client-side dynamic prop.
prop_player_cosmetic
Cosmetic player prop.
ambient_creatures
Client-side dynamic prop.
ambient_creatures_zone
Client-side func_brush.
world_bounds
Defines camera constraint coordinates.
dota_building
dota_building
npc_dota_holdout_tower_reducespeed
(used in Frostivus event) Creates a tower that reduces enemy movespeed. This can also be edited in the npc_units.txt file.
ent_dota_halloffame
The Hall of Fame.
info_courier_spawn_radiant
Spawns a Radiant team courier.
info_courier_spawn_dire
Spawns a Dire team courier.
trigger_shop
Region that defines the shop radius.
trigger_no_wards
Wards disallowed here.
trigger_boss_attackable
Boss (Roshan) is attackable from here.
trigger_hero
A trigger that's fired when a hero touches it.
tutorial_npc_blocker
The start or endpoint of an NPC blocking line.
info_roquelaire_perch
(used in Dota2 PvP tutorial) A perch point for Roquelaire to sit on.
env_global_light
Defines global light settings.
env_deferred_light
Deferred light entity.
env_deferred_spot_light
Deferred spot light.