Page 1 of 1

Can you make an entity despawn after a while?

Posted: Mon Apr 24, 2023 5:34 pm
by Leoncio
Hello,

I was wondering if there is way to make a "simple-entity" have a duration/despawn -timer? So far I haven't been able to find a way to make them disappear after a couple of seconds. The only things that I have been able to find, are ghosts, character-corpse, smoke-with-trigger, but none of them has the commands that I am looking for.

Re: Can you make an entity despawn after a while?

Posted: Mon Apr 24, 2023 8:40 pm
by Pi-C
Leoncio wrote: Mon Apr 24, 2023 5:34 pm Hello,

I was wondering if there is way to make a "simple-entity" have a duration/despawn -timer?
Hi! I think you must use control scripting. Listen to the entity-created events and store the new entities in your global table:

Code: Select all

local function register_entity(event)
	local tick = event.tick
	local entity =	event.entity or				-- script_raised_built
				event.created_entity or 		-- on_built_entity
				event.destination			-- on_entity_cloned

	local remove_tick = tick + 5*60*60 			-- 5 minutes (60 ticks per second, 60 seconds per minute)
	
	if not global.remove_at then
		global.remove_at = {}
	end
	
	global.remove_at[remove_tick] = global.remove_at[remove_tick] or {}
	table.insert(global.remove_at[remove_tick], entity)
end

script.on_event(defines.events.script_raised_built, register_entity)
script.on_event(defines.events.on_built_entity, register_entity)
script.on_event(defines.events.on_entity_cloned, register_entity)
Then listen to on_tick and check if any entities are scheduled for removal:

Code: Select all

local function remove_entities(event)
	local remove = global.remove_at and global.remove_at[event.tick]
	
	if remove then
		for e, entity in pairs(remove) do
			if entity.valid then
				entity.destroy()
			end
		end
		global.remove_at[event.tick] = nil
		if not next(global.remove_at[event.tick] then
			global.remove_at = nil
		end
	end
end

script.on_event(defines.events.on_tick, remove_entities)
This is the most simple way I can think of. You can improve the performance by making sure that on_tick will only run when it's necessary -- i.e. if global.remove_at exists.

Re: Can you make an entity despawn after a while?

Posted: Tue Apr 25, 2023 11:48 pm
by Leoncio
Thanks for the reply and help! :D
I am so lost in what the piece of code that you posted means, which makes me unable to implement my entity to it, this is my entity that I want to give a despawn timer, how would the script code look like with this in mind?

Code: Select all

data:extend({
    {
        type = "simple-entity",
        name = "rock-gun-block",
        flags = {"placeable-neutral", "placeable-off-grid"},
        map_color = {r = 40, g = 110, b = 245, a = 0.7},
        icon = "__rock_gun__/graphics/rock/rock_icon.png",
        icon_size = 64, icon_mipmaps = 4,
        subgroup = "grass",
        collision_box = {{-0.24, -0.24},{0.24, 0.24}},
        collision_mask = {"player-layer", "train-layer"},
        selection_box = {{-0.5, -0.5},{0.5, 0.5}},
        render_layer = "object",
        count_as_rock_for_filtered_deconstruction = true,
        mined_sound = { filename = "__base__/sound/deconstruct-bricks.ogg" },
        vehicle_impact_sound = { filename = "__base__/sound/car-stone-impact.ogg", volume = 0.5 },
        max_health = 300,
        pictures =
        {
        },
    }
   })

Re: Can you make an entity despawn after a while?

Posted: Wed Apr 26, 2023 10:28 pm
by Pi-C
Leoncio wrote: Tue Apr 25, 2023 11:48 pm Thanks for the reply and help! :D
I am so lost in what the piece of code that you posted means, which makes me unable to implement my entity to it, this is my entity that I want to give a despawn timer, how would the script code look like with this in mind?

Code: Select all

data:extend({
    {
        type = "simple-entity",
        name = "rock-gun-block",
        …
   })
When you register an event handler, it will be called with the event data listed in the description of each event as a parameter. All events provide "name" (the numeric ID from defines.events[event.name]) and "tick", and all events except for on_tick provide at least one additional parameter. Now, on_built_entity, on_entity_cloned, and script_raised_built all provide the new entity, but they call it differently. Therefore, I used

Code: Select all

	local entity =	event.entity or				-- script_raised_built
				event.created_entity or 		-- on_built_entity
				event.destination			-- on_entity_cloned
in register_entity() to extract the new entity.

The table global.remove_at will only exist if at least one entity is scheduled for removal. Therefore, we must make sure the table is initialized before writing to it:

Code: Select all

if not global.remove_at then
		global.remove_at = {}
end
This could be shortened to

Code: Select all

global.remove_at = global.remove_at or {}
But if you want to activate on_tick only when its needed (see below), the if-then construct is more convenient.

It's possible that several of your entities are created on the same tick, so global.remove_at[tick] should be a table to which you add all entities.

In remove_entities(event), the only parameters are {tick = uint, name = defines.events.on_tick}. We use

Code: Select all

	local remove = global.remove_at and global.remove_at[event.tick]
to check that global.remove_at exists at all, and that at least one entity is scheduled for removal on this tick. If that is true, we loop over all entities in global.remove_at[event.tick] and destroy them if they are still valid. When this is done, we delete global.remove_at[event.tick], and when the last entry has been removed, we delete global.remove_at. If you want to use on_tick dynamically, then this would be the place to disable the event.

Now which entities do we react to? In the current state, the event would trigger whenever any entity is created, be it transport belts, assembling machines, cars, or even flying text. You obviously want to limit that. One way to do this is adding this at the start of the function register_entity:

Code: Select all

if not (entity.valid and entity.name == "rock-gun-block") then
	return
end
That's feasible if you're interested in a number of different prototypes. For example, Autodrive has a feature that cars under its control will be healed when they crash into a tree, or that entities belonging to the same force as the car that damaged them will get back their full health -- so I must look at all entities that take damage. But in your case, you're really interested in only one prototype, and there's a better way for limiting the event: using event filters. This way, filtering is done directly by the game, which is faster than running Lua code for checking:

Code: Select all

local filters = { 
	{type = "name", name = "rock-gun-block"}, 
}
script.on_event(defines.events.on_built_entity, register_entity, filters)
script.on_event(defines.events.on_entity_cloned, register_entity, filters)
script.on_event(defines.events.script_raised_built, register_entity, filters)
Here, each line in "filters" is an array of filter conditions. If you wanted to do that, you could add another line like

Code: Select all

{type = "force", force = "neutral"}
The two filters would be ORed together, so the event would trigger whenever your entity OR any entity belonging to force "neutral" was created.

Finally, the handler for on_tick is activated. That should be everything that's needed in the most simple case where on_event is always running. It's not necessary to listen to on_entity_died etc.: If one of your entities is destroyed before its scheduled removal, it will be removed from the table when the function remove_entities is run for the scheduled tick.

However, while simple, this approach isn't really efficient (e.g., on_tick could run for an hour although none of your entities has been created). It's far better to register the event when an entity has been created, or in on_load:

Code: Select all

local function register_entity(event)
	…
	if not global.remove_at then
		global.remove_at = {}
		script.on_event(defines.events.on_tick, remove_entities)
	end
	…
end

script.on_load(function()
	if global.remove_at then
		script.on_event(defines.events.on_tick, remove_entities)
	end
end)
and to turn it off when the last scheduled entity has been removed:

Code: Select all

local function remove_entities(event)
	…
		global.remove_at[event.tick] = nil
		if not next(global.remove_at[event.tick] then
			global.remove_at = nil
			script.on_event(defines.events.on_tick, nil)
		end
	…
end
If you do this, you should also listen to on_entity_died, on_entity_destroyed, on_player_mined_entity, on_robot_mined_entity, and script_raised_destroy and remove the entity from global.remove_at[event.tick], and global.remove_at[event.tick] if no other entity removal is scheduled for event.tick, and global.remove_at if it is empty. But that's a bit tricky (e.g. script_raised_destroy will be raised after the after the entity has been destroyed, so you couldn't look up its unit_number anymore) and explaining that as well would mean putting up another wall of text, for which I'm too tired now. :mrgreen:

Re: Can you make an entity despawn after a while?

Posted: Sat Apr 29, 2023 10:07 am
by Leoncio
Thanks for the reply!

I've been trying to implement the script as good as I can, but I still getting error messages that I don't understand what it means.

the code that I have been running in the control.lua is

Code: Select all

local function register_entity(event)
	local tick = event.tick
	local entity =	event.entity or				-- script_raised_built
				event.created_entity or 		-- on_built_entity
				event.destination			-- on_entity_cloned

	local remove_tick = tick + 5*60--5*60*60 			-- 5 minutes (60 ticks per second, 60 seconds per minute)
	
	if not global.remove_at then
		global.remove_at = {}
		script.on_event(defines.events.on_tick, remove_entities)
	end
	
	global.remove_at[remove_tick] = global.remove_at[remove_tick] or {}
	table.insert(global.remove_at[remove_tick], entity)
end

local filters = {
    {type = "name", name = "rock-gun-block"},
}
script.on_event(defines.events.on_built_entity, register_entity, filters)
script.on_event(defines.events.on_entity_cloned, register_entity, filters)
script.on_event(defines.events.script_raised_built, register_entity, filters)

local function remove_entities(event)
	local remove = global.remove_at and global.remove_at[event.tick]
	
	if remove then
		for e, entity in pairs(remove) do
			if entity.valid then
				entity.destroy()
			end
		end
		global.remove_at[event.tick] = nil
		if not next(global.remove_at[event.tick]) then
			global.remove_at = nil
			script.on_event(defines.events.on_tick, nil)
		end
	end
end

script.on_event(defines.events.on_tick, remove_entities)
But I am getting an error box when loading a new map, the message is: "Key "filter" not found in property tree at ROOT[0]"
Any idea what it means?

Re: Can you make an entity despawn after a while?

Posted: Sat Apr 29, 2023 10:25 am
by Pi-C
Leoncio wrote: Sat Apr 29, 2023 10:07 am I am getting an error box when loading a new map, the message is: "Key "filter" not found in property tree at ROOT[0]"
Any idea what it means?
Sorry, there was a typo in my code! It must be:

Code: Select all

local filters = {
    {filter = "name", name = "rock-gun-block"},
}
script.on_event(defines.events.on_built_entity, register_entity, filters)
script.on_event(defines.events.on_entity_cloned, register_entity, filters)
script.on_event(defines.events.script_raised_built, register_entity, filters)

Re: Can you make an entity despawn after a while?

Posted: Sat Apr 29, 2023 11:53 am
by Leoncio
Thank you!

I will load now, however, the entity wont despawn. Should the entire code be in control.lua? Should I do something else to the entity to make it interact with the event script?

Re: Can you make an entity despawn after a while?

Posted: Sat Apr 29, 2023 1:04 pm
by Pi-C
Leoncio wrote: Sat Apr 29, 2023 11:53 am I will load now, however, the entity wont despawn. Should the entire code be in control.lua?
Yes, all of that should be in control.lua (or in a file loaded by it via "require()").
Should I do something else to the entity to make it interact with the event script?
If you place down a new entity, is it stored in the global table? Add this at the top of control.lua:

Code: Select all

-- Support for "Global variable viewer"
if script.active_mods["gvv"] then
  require("__gvv__.gvv")()
  log("Enabled support for gvv!")
end
Then add this mod, reload the game, and type "/gvv" in the chat window to open the GUI of gvv. You can then inspect the contents of your global table. Also, you should add "log()" commands to your code so you can follow what happens and see whether the part that should destroy the entities is actually run.

Re: Can you make an entity despawn after a while?

Posted: Sat Apr 29, 2023 2:46 pm
by Leoncio
Pi-C wrote: Sat Apr 29, 2023 1:04 pm Then add this mod, reload the game, and type "/gvv" in the chat window to open the GUI of gvv. You can then inspect the contents of your global table. Also, you should add "log()" commands to your code so you can follow what happens and see whether the part that should destroy the entities is actually run.
What would the code for adding log() to my code be?

Re: Can you make an entity despawn after a while?

Posted: Sat Apr 29, 2023 4:46 pm
by Pi-C
Leoncio wrote: Sat Apr 29, 2023 2:46 pm What would the code for adding log() to my code be?
Look here for a description of the "serpent" library and "log()" function. You could log just a short fixed-text message to indicate which branch of an if-else-then statement has been taken, you could log the value of a variable or the contents of a table, or what arguments have been passed on to a function. Here's an example:

Code: Select all

local function register_entity(event)
  log("Entered function register_entity("..serpent.line(event)..")")

	local tick = event.tick
	local entity =	event.entity or				-- script_raised_built
				event.created_entity or 		-- on_built_entity
				event.destination			-- on_entity_cloned
  log(string.format("entity: %s \"%s\" (%s)",
                    entity and entity.type or "nil",
                    entity and entity.name or "nil",
                    entity and entity.unit_number or "nil"))

	local remove_tick = tick + 5*60--5*60*60 			-- 5 minutes (60 ticks per second, 60 seconds per minute)
  log("remove_tick: "..remove_tick)

  
	if not global.remove_at then
    log("Creating global.remove_at!")
		global.remove_at = {}
		script.on_event(defines.events.on_tick, remove_entities)
	end
	
	global.remove_at[remove_tick] = global.remove_at[remove_tick] or {}
	table.insert(global.remove_at[remove_tick], entity)

  log("global.remove_at: "..serpent.block(global.remove_at))

  log("Leaving function register_entity()")
end

Actually, it's a good idea to put a wrapper around log(), so that you can turn it off when you release your mod. Logging is useful while you're working on your mod, but you shouldn't spam the log files of your users. The wrapper could be as simple as this (insert at top of control.lua):

Code: Select all

-- Change value to "false" before releasing the mod!
ENABLE_LOGGING = true		

local my_log 
if ENABLE_LOGGING then
	my_log = function(msg) 
		log(msg)
	end
else
	my_log = function() end
end
In the function above, you'd then replace all instances of "log()" with "my_log()":

Code: Select all

my_log("Creating global.remove_at!")
Depending on the state of "ENABLE_LOGGING", the message you've passed on will be either written to the log file (factorio-current.log) or discarded.