Adding the continuous burning property to a building?

Place to get help with not working mods / modding interface.
LuxArdens
Burner Inserter
Burner Inserter
Posts: 11
Joined: Tue Nov 12, 2019 11:26 pm
Contact:

Adding the continuous burning property to a building?

Post by LuxArdens »

Hi, I'm trying to make a type of building for my mod that requires and consumes a certain "fuel" constantly to keep a bar from depleting, regardless of whether it is actually working on a recipe at that moment or not. I thought before that this would be pretty easy since the nuclear reactor is also 'always on' and will just continue to ask for fuel no matter if the temperature is at 1000 or not. But now I can't actually find out how this is done, let alone duplicate it. Is this possible at all, and if so how?

To clarify further:
- I'm aware this is usually avoided to make entities sleep and save UPS. The building I have planned will be strictly limited in number so I accept the tiny hit to UPS if they cannot sleep.

- The idea behind this building is it simulates a population that needs to be fed constantly. It starts 100% filled with food/fuel, and as long as the food/fuel bar is above 99%, it will operate, but if it drops below it stops crafting products and will gradually amass a huge food deficit until the food supply is sufficient again for a long time. I don't see an easier way of implementing this without the envisioned constant fuel consumption, but if anyone has a better idea to do it I'm all ears.

Update: I tried to use the reactor type instead of an assembler, and while that does burn the food continuously, I can't make it produce a recipe or anything but heat. Is there some way to make a reactor do recipes? Or do I have to make some contraption with hidden entities and link the temperature of the reactor to an assembler to switch said assembler on and off?
LuxArdens
Burner Inserter
Burner Inserter
Posts: 11
Joined: Tue Nov 12, 2019 11:26 pm
Contact:

Re: Adding the continuous burning property to a building?

Post by LuxArdens »

Okay update: I see now the usual invisible entity contraption is the only solution. So far I've got this after repairing some insanely broken code I found elsewhere on the forum:

Code: Select all

-- call script.onevent, tell it we want to do something when an entity is built, and give another anonymous function that will contain the code to actually do stuff
script.on_event(defines.events.on_built_entity, function(event)
  if event.created_entity.name == "housing-1" then -- if the entity that the player built was a house
    -- insert into the global.housing1 table an entry with a 'key' of housing that is the housing-1 entity, and a key of housing-reactor that is a new housing-1-reactor entity at the same position
    table.insert( global.housing1, { housing1=event.created_entity, housing1reactor=game.surfaces[1].create_entity{ name="housing-1-reactor", position=event.created_entity.position, force = game.forces.player}})
  end
end)
script.on_event(defines.events.on_tick, function(event) -- will change it to check way less than once per second later on
  if event.tick % 60 == 0 then
    for index, thetable in ipairs(global.housing1) do -- loop through the housing1 table
      -- if the player built housing no longer exists
      if not thetable.housing1.valid then
        -- safety check
        if thetable.housing1reactor.valid then
          thetable.housing1reactor.destroy() -- destroy reactor
          table.remove(global.housing1, index) -- remove the current table (housing and reactor) from the glob.housing table.
        end
      end
    end
  end
end
)
Which successfully spawns an invisible reactor entity "housing-1-reactor" underneath every assembler "housing-1" and keeps track, and it deletes them successfully.

Having zero real experience with LUA this is stretching my abilities though. Could anyone give me pointers on how to periodically check the current heat value on the reactors, compare them to some number, and then disable their linked assembler if it drops below?
LuxArdens
Burner Inserter
Burner Inserter
Posts: 11
Joined: Tue Nov 12, 2019 11:26 pm
Contact:

Re: Adding the continuous burning property to a building?

Post by LuxArdens »

Update 2:

I figured it out! It's probably a disgusting piece of code, it will probably kill the UPS, but it works! God, I'm so happy, I love life and from now on I love Lua as well.

Code: Select all

-- call script.onevent, tell it we want to do something when an entity is built, and give another anonymous function that will contain the code to actually do stuff
script.on_event(defines.events.on_built_entity, function(event)
  if event.created_entity.name == "housing-1" then -- if the entity that the player built was a house
    -- insert into the global.housing1 table an entry with a 'key' of housing that is the housing-1 entity, and a key of housing-reactor that is a new housing-1-reactor entity at the same position
    table.insert( global.housing1, { housing1=event.created_entity, housing1reactor=game.surfaces[1].create_entity{ name="housing-1-reactor", position=event.created_entity.position, force = game.forces.player}})
  end
end)
script.on_event(defines.events.on_tick, function(event) -- will change it to check way less than once per second later on
  if event.tick % 60 == 0 then
    for index, thetable in ipairs(global.housing1) do -- loop through the housing1 table
      -- if the player built housing no longer exists
      if not thetable.housing1.valid then
        if thetable.housing1reactor.valid then
          thetable.housing1reactor.destroy() -- destroy reactor
          table.remove(global.housing1, index) -- remove the current table (housing and reactor) from the glob.housing table.
        end
		-- elseif reactor underneath house no longer exists, destroy the house above it
      elseif not thetable.housing1reactor.valid then
        if thetable.housing1.valid then
          thetable.housing1.destroy() -- destroy reactor
          table.remove(global.housing1, index) -- remove the current table (housing and reactor) from the glob.housing table.
        end
	  -- now since both still exist, add elseif statement checking temperature is above 60%
	  elseif thetable.housing1reactor.temperature < 600 then
		thetable.housing1reactor.damage(1, game.forces.enemy, "impact") -- I probably won't be keeping this in, but it's useful for visually tracking things
		-- now reduce the heat in each reactor since there is no actual heat loss mechanic
		thetable.housing1reactor.temperature = thetable.housing1reactor.temperature - 6 -- pay a little less when the housing is not operational, allowing an increase in temperature
		thetable.housing1.crafting_progress = 0 -- you lose all crafting process
	  elseif thetable.housing1reactor.temperature > 600 then
		thetable.housing1reactor.temperature = thetable.housing1reactor.temperature - 7 -- pay full maintenance when the housing is operational; it will still slowly increase
      end
    end
  end
end
)
Pi-C
Smart Inserter
Smart Inserter
Posts: 1734
Joined: Sun Oct 14, 2018 8:13 am
Contact:

Re: Adding the continuous burning property to a building?

Post by Pi-C »

LuxArdens wrote: Sun Sep 24, 2023 9:37 pm Update 2:

I figured it out! It's probably a disgusting piece of code, it will probably kill the UPS, but it works!
Some improvements:

Code: Select all

script.on_event(defines.events.on_built_entity, function(event)
  if event.created_entity.name == "housing-1" then -- if the entity that the player built was a house
    -- insert into the global.housing1 table an entry with a 'key' of housing that is the housing-1 entity, and a key of housing-reactor that is a new housing-1-reactor entity at the same position
    table.insert( global.housing1, { housing1=event.created_entity, housing1reactor=game.surfaces[1].create_entity{ name="housing-1-reactor", position=event.created_entity.position, force = game.forces.player}})
  end
end)
There are more events you should listen to! They are raised with different arguments, so you must adjust the values.

About your function: You should make sure that housing and reactor are on the same surface (which is not guaranteed to be game.surfaces[1] as mods like Factorissimo or SE will create others).

Code: Select all

-- Look for invalid entities every CLEANUP_DELAY ticks
CLEANUP_DELAY = 60

function register_entities(event)
  -- We use the same function for different events, so we must cover all different
  -- key names to get the entity!
  local entity = event.created_entity or -- on_built_entity/on_robot_built_entity
                  event.destination or    -- on_entity_cloned
                  event.entity            -- script_raised_*

  local player = event.player_index and game.get_player(event.player_index)
  local robot = event.robot

  local force = (player and player.valid and player.force) or
                (robot and robot.valid and robot.force) or
                entity.force

  local surface = entity.surface

  if force and force.valid and surface and surface.valid then

    -- If this is the first entity, activate on_tick and on_nth_tick!
    if not next(global.housing1) then
      local enable = true
      toggle_tick_events(enable)
    end

    -- Create reactor
    local reactor = surface.create_entity{
      name = "housing-1-reactor",
      position = entity.position,
      force = force,
      create_build_effect_smoke = false,
    }

    -- insert into the global.housing1 table an entry with a 'key' of housing that is the housing-1 entity, and a key of housing-reactor that is a new housing-1-reactor entity at the same position
    table.insert( global.housing1, { housing1 = entity, housing1reactor = reactor })
  end
end
Regarding your on_tick event, I'd only use it for checking the reactor temperature and clean up the table in on_nth_tick. That will save you from running the timer in Lua.

Code: Select all

function cleanup_entities()
  -- Removing elements from a table while looping over it with pairs() or ipairs() is
  -- not reliable because it will mess with the iterator. Instead, check a copy of the
  -- table and use the index to remove elements from the original.
  local copy = table.deepcopy(global.housing1)
  for index, thetable in ipairs(copy) do
    if not (thetable.housing1 and thetable.housing1.valid) then
      if (thetable.housing1reactor and thetable.housing1reactor.valid) then
        thetable.housing1reactor.destroy() -- destroy reactor
        table.remove(global.housing1, index) -- remove the current table (housing and reactor) from the glob.housing table.
      end
		-- elseif reactor underneath house no longer exists, destroy the house above it
    elseif not (thetable.housing1reactor and thetable.housing1reactor.valid) then
      if thetable.housing1.valid then
        thetable.housing1.destroy() -- destroy reactor
        table.remove(global.housing1, index) -- remove the current table (housing and reactor) from the glob.housing table.
      end
    end
  end

  -- If the last entity has been removed, cancel on_tick and on_nth_tick!
  if not next(global.housing1) then
    local enable = false
    toggle_tick_events(enable)
  end
end


function check_temperature(event)
  for index, thetable in ipairs(global.housing1) do -- loop through the housing1 table
    if thetable.housing1reactor.temperature < 600 then
      -- I probably won't be keeping this in, but it's useful for visually tracking things
      thetable.housing1reactor.damage(1, game.forces.enemy, "impact") 

      -- now reduce the heat in each reactor since there is no actual heat loss mechanic
      thetable.housing1reactor.temperature = thetable.housing1reactor.temperature - 6 -- pay a little less when the housing is not operational, allowing an increase in temperature
      thetable.housing1.crafting_progress = 0 -- you lose all crafting process

    elseif thetable.housing1reactor.temperature > 600 then
      thetable.housing1reactor.temperature = thetable.housing1reactor.temperature - 7 -- pay full maintenance when the housing is operational; it will still slowly increase
    end
  end
end

So far, there are just some functions that should be called in response to events. Now you need an event manager that will activate all necessary events:

Code: Select all

-- Enable/disable on_tick and on_nth_tick. Must be called from on_load and when the first entity has been built or the last entity has been removed.
function toggle_tick_events(enable)
  if enable then
    log("Activating on_tick!")
    script.on_event(defines.events.on_tick, check_temperature)
    log("Activating on_nth_tick!")
    script.on_nth_tick(CLEANUP_DELAY, cleanup_entities)
  else
    log("TUrning off on_tick!")
    script.on_event(defines.events.on_tick, nil)
    log("TUrning off  on_nth_tick!")
    script.on_nth_tick(CLEANUP_DELAY, nil)
  end
end

function init()
  global.housing1 = global.housing1 or {}
end

script.on_init(init)

script.on_configuration_changed(function(event)
  init()
  cleanup_entities()

  local enable = next(global.housing1) and true or false
  toggle_tick_events(enable)
end)

script.on_load(function()
  local enable = global.housing1 and next(global.housing1) and true or false
  toggle_tick_events(enable)
end)

local build_events = {
  "on_built_entity",
  "on_robot_built_entity",
  "on_entity_cloned",
  "script_raised_built",
  "script_raised_revive"
}
local event_filters = {
  {filter = "name", name = "housing-1"},
}
for e, event in ipairs(build_events) do
  log("Registering handler for "..event)
  script.on_event(defines.events[event], register_entities, event_filters)
end
A good mod deserves a good changelog. Here's a tutorial (WIP) about Factorio's way too strict changelog syntax!
Pi-C
Smart Inserter
Smart Inserter
Posts: 1734
Joined: Sun Oct 14, 2018 8:13 am
Contact:

Re: Adding the continuous burning property to a building?

Post by Pi-C »

The logic for the clean-up function was wrong, better use this instead:

Code: Select all

function cleanup_entities()
  -- Removing elements from a table while looping over it with pairs() or ipairs() is
  -- not reliable because it will mess with the iterator. Instead, check a copy of the
  -- table and use the index to remove elements from the original.
  -- EDIT: Instead, move valid entities to temporary table and use that to replace
  -- the original when done.
  local tmp = {}
  for index, thetable in ipairs(global.housing1) do
    if not (thetable.housing1 and thetable.housing1.valid) then
      log(index..": Invalid housing")
      if (thetable.housing1reactor and thetable.housing1reactor.valid) then
        thetable.housing1reactor.destroy() -- destroy reactor
        --~ table.remove(global.housing1, index) -- remove the current table (housing and reactor) from the glob.housing table.
      end
		-- elseif reactor underneath house no longer exists, destroy the house above it
    elseif not (thetable.housing1reactor and thetable.housing1reactor.valid) then
      log(index..": Invalid reactor")
      if thetable.housing1.valid then
        thetable.housing1.destroy() -- destroy reactor
        --~ table.remove(global.housing1, index) -- remove the current table (housing and reactor) from the glob.housing table.
      end
    else
      log(index..": Keeping housing and reactor")
      table.insert(tmp, thetable)
    end
  end
  global.housing1 = table.deepcopy(tmp)

  -- If the last entity has been removed, cancel on_tick and on_nth_tick!
  if not next(global.housing1) then
    local enable = false
    toggle_tick_events(enable)
  end
end
A good mod deserves a good changelog. Here's a tutorial (WIP) about Factorio's way too strict changelog syntax!
LuxArdens
Burner Inserter
Burner Inserter
Posts: 11
Joined: Tue Nov 12, 2019 11:26 pm
Contact:

Re: Adding the continuous burning property to a building?

Post by LuxArdens »

There are more events you should listen to! They are raised with different arguments, so you must adjust the values.

About your function: You should make sure that housing and reactor are on the same surface (which is not guaranteed to be game.surfaces[1] as mods like Factorissimo or SE will create others).
Thanks a lot! So bots placing entities I understand, but when does cloning happen for normal assembler type buildings? And script_raised is for interaction with other mods if I understand correctly?

This will be part of an overhaul mod so it will never play nice with SE, and if something like Factorissimo ends up on the blacklist then that's not a big issue per se, but I will be sure to check the other surfaces as you suggest regardless.
Regarding your on_tick event, I'd only use it for checking the reactor temperature and clean up the table in on_nth_tick. That will save you from running the timer in Lua.
Okay. I probably don't need it to run every second either, I determined that the heat removal can occur once every 300+ ticks and the temperature check may also be less frequent than once every 60 ticks with some adjustments. Does that save UPS or is it worse because you're now running another timer in Lua?

Nvm, just saw that on_nth_tick can be >60 just fine.
So far, there are just some functions that should be called in response to events. Now you need an event manager that will activate all necessary events:
I'm not getting this to work yet but I will put some more time in it. Is all of this supposed to go into control.lua or do the functions go into the scripts folder and then you call them from control.lua?
Pi-C
Smart Inserter
Smart Inserter
Posts: 1734
Joined: Sun Oct 14, 2018 8:13 am
Contact:

Re: Adding the continuous burning property to a building?

Post by Pi-C »

LuxArdens wrote: Mon Sep 25, 2023 1:04 pm Thanks a lot! So bots placing entities I understand, but when does cloning happen for normal assembler type buildings? And script_raised is for interaction with other mods if I understand correctly?
Mods can use LuaEntity::clone, players can clone stuff in editor mode (enter '/editor' on the chat console):
clone.png
clone.png (73.02 KiB) Viewed 1244 times
Regarding your on_tick event, I'd only use it for checking the reactor temperature and clean up the table in on_nth_tick. That will save you from running the timer in Lua.
Okay. I probably don't need it to run every second either, I determined that the heat removal can occur once every 300+ ticks and the temperature check may also be less frequent than once every 60 ticks with some adjustments. Does that save UPS or is it worse because you're now running another timer in Lua?

Generally speaking, whatever is done directly by the game runs faster that whatever mods can do: The game is programmed in C, while Lua code has to be interpreted and mods must get data from/send data to the game before it can be used. So when you run

Code: Select all

if (event.tick % n) == 0
in on_tick, there are 2 Lua operations involved (calculating the value and comparing it to 0) each time that are not necessary if you only want to run the code behind it every n ticks (assuming that n>1). I suppose on_nth_tick would run the same operations but be faster because they are run by the game. Also, if you're logging stuff while debugging, it definitely makes a difference whether you spam 1 message per tick or 1 message per n ticks. :-)

The Lua vs. C stuff is also why I prefer using event.filters over things like "if event.entity.name == "housing-1". The latter may still be necessary if you want to run different code branches for different prototypes, but that's just for distinguishing between the different types of entities you care about, not for filtering out the types of entities you want to ignore.

Nvm, just saw that on_nth_tick can be >60 just fine.
Even better, you can pass on a list of values, so you can use several timers out of on_tick! In addition to performance reasons, keeping different things (those you want to run each tick vs. those you want to ignore most of the time) should make it easier to read the code.

Code: Select all

script.on_nth_tick({60, 70, 80}, function(event)
  local t = event.nth_tick
  if  t == 60 then
  	…
  elseif t == 70 then
  	…
  else
  	…
  end
end)

So far, there are just some functions that should be called in response to events. Now you need an event manager that will activate all necessary events:
I'm not getting this to work yet but I will put some more time in it. Is all of this supposed to go into control.lua or do the functions go into the scripts folder and then you call them from control.lua?
You don't have too much code yet, so I'd say keep everything in control.lua. Once that is growing too long it may make sense to restructure things and put functions that belong together into different files:

Code: Select all

-- scripts.gui
local gui_stuff = {}
gui_stuff.create_gui = function(event)
	…
end

gui_stuff.delete_gui = function(event)
	…
end

return gui_stuff



-- scripts.entities
local entity = {}
entity.register = function(event)
	…
end

entity.cleanup = function()
	…
end
return entity



-- control.lua
my_gui = require("scripts.gui") 	-- Runs file "gui.lua" from folder "scripts" located at the toplevel of your mod and assigns the return value to "my_gui"
entities = require(scripts.entities")

script.on_event(defines.events.on_entity_built, entities.register)
A good mod deserves a good changelog. Here's a tutorial (WIP) about Factorio's way too strict changelog syntax!
LuxArdens
Burner Inserter
Burner Inserter
Posts: 11
Joined: Tue Nov 12, 2019 11:26 pm
Contact:

Re: Adding the continuous burning property to a building?

Post by LuxArdens »

You don't have too much code yet, so I'd say keep everything in control.lua. Once that is growing too long it may make sense to restructure things and put functions that belong together into different files
Alright cheers. It's probably going to take me some time to integrate some of these tips but at least I added the bot/clone stuff and the nth_tick finally works and after testing it, it seems the current script uses very little CPU time even when there are 200 house entities on the map (and there are supposed to be 100-150 max regardless of base size) so it appears this won't be much of a problem.

Unfortunately it seems inserters lock on to a single building, so the combined house entity will require 2 inserters to feed it:

Image
(Blender? What do I need a blender for?)
FuryoftheStars
Smart Inserter
Smart Inserter
Posts: 2768
Joined: Tue Apr 25, 2017 2:01 pm
Contact:

Re: Adding the continuous burning property to a building?

Post by FuryoftheStars »

Re: putting functions off into other files - You only really need to do this if the functions you are creating are also going to be called from yet other files (and even then, not really). Some people do like to put them off into other files anyway to keep the main one "clean", but personally, I find it easier to read the code when everything's together in one file. YMMV.
My Mods: Classic Factorio Basic Oil Processing | Sulfur Production from Oils | Wood to Oil Processing | Infinite Resources - Normal Yield | Tree Saplings (Redux) | Alien Biomes Tweaked | Restrictions on Artificial Tiles | New Gear Girl & HR Graphics
Pi-C
Smart Inserter
Smart Inserter
Posts: 1734
Joined: Sun Oct 14, 2018 8:13 am
Contact:

Re: Adding the continuous burning property to a building?

Post by Pi-C »

FuryoftheStars wrote: Tue Sep 26, 2023 11:35 pm Re: putting functions off into other files - You only really need to do this if the functions you are creating are also going to be called from yet other files
It depends. You may run get problems if local function A is calling local function B (so it must be defined before A) and B is calling local function C (so C must be defined before B), but C may also want to call A. That becomes a non-issue if A, B, and C are defined in one table: If you have functions my_gui.A, my_gui.B, and my_gui.C, it doesn't matter which one is defined first.
(and even then, not really)
Right, if you make all your functions global, their order wouldn't matter either -- whether you keep those functions in one file or spread them over several files.
Some people do like to put them off into other files anyway to keep the main one "clean", but personally, I find it easier to read the code when everything's together in one file. YMMV.
Autodrive used to have a monolithic control file that contained everything: creating/updating the GUI, handling of the cars, remote interface, and reacting to events. I collaborated and later inherited the mod, and because I really wasn't too familiar with the code, commented out old stuff for reference and rewrote/added to it, so the file was growing more and more. Let me tell you, it is not a pleasure to debug code if you have to scroll from the start to the end (and back) of a 3000+ lines file all the time! Restructuring the code by splitting it into several files was like going through hell, but IMHO it was absolutely worth it. Maintaining code and adding functions is so much easier now. But I agree, keeping everything in one file will probably make it easier to read the code if that file isn't too long. :-)
A good mod deserves a good changelog. Here's a tutorial (WIP) about Factorio's way too strict changelog syntax!
Post Reply

Return to “Modding help”