Subsurface - build your base underground!

by Natha

While everyone is looking up to space, other planets and the rest of the universe, there is a whole world buried directly underneath us. Use this opportunity to declutter your factory logistics by routing it under your base and explore the underground!

Content
2 days ago
1.1 - 2.0
6.63K
Transportation Logistics Trains Environment Mining

b [Fixed] Extremely expensive script.on_configuration_changed

21 days ago

I and a friend of mine were developing mods and testing mod compatibility alongside this mod, and we discovered that Checking consistency stage of the game takes unusually long time. It took me more than half a minute, and for my friend, several minutes.

I backtraced the game and found that it is due to this mod's script.on_configuration_changed, in particular, these lines:

        for _, t in ipairs(s.find_tiles_filtered{name = "out-of-map", has_hidden_tile = true}) do
            if t.hidden_tile == "grass-1" then
                s.set_hidden_tile(t.position, substitute)
                found = true
            end
        end

I tried to get timing info, using

@@ -117,12 +117,19 @@ script.on_configuration_changed(function(config) -- TBC
                        found = true
                end
                s.set_tiles(new_tiles)
+               print('before find_tiles_filtered')
+               local i = 0
                for _, t in ipairs(s.find_tiles_filtered{name = "out-of-map", has_hidden_tile = true}) do
+                       if i == 0 then
+                               print('first iteration')
+                       end
+                       i = i + 1
                        if t.hidden_tile == "grass-1" then
                                s.set_hidden_tile(t.position, substitute)
                                found = true
                        end
                end
+               print('end', i, 'iteration')
        end
        if found then game.print("[font=default-large-bold][color=yellow]Subsurface: At least one tile generated in subsurfaces was removed from the game due to your mod configuration c
hanges. It has been replaced with dirt-like tiles.[/color][/font]") end
 end)

and this is the output:

Dec 20 02:04:36 before find_tiles_filtered
Dec 20 02:04:44 first iteration
Dec 20 02:04:45 end 3043007 iteration
Dec 20 02:04:45 before find_tiles_filtered
Dec 20 02:04:53 first iteration
Dec 20 02:04:54 end 2655697 iteration
Dec 20 02:04:54 before find_tiles_filtered
Dec 20 02:05:01 first iteration
Dec 20 02:05:01 end 2330559 iteration
Dec 20 02:05:01 before find_tiles_filtered
Dec 20 02:05:09 first iteration
Dec 20 02:05:10 end 3814948 iteration
Dec 20 02:05:10 before find_tiles_filtered
Dec 20 02:05:10 first iteration
Dec 20 02:05:10 end 760272  iteration
Dec 20 02:05:10 before find_tiles_filtered
Dec 20 02:05:11 first iteration
Dec 20 02:05:11 end 833028  iteration
Dec 20 02:05:11 before find_tiles_filtered
Dec 20 02:05:11 first iteration
Dec 20 02:05:11 end 50625   iteration

As I understand out-of-map is for chunks that have been generated, but not revealed by mining the neighboring rocks. However, Factorio slowly generates a square of 39x39 chunks around the player, with player in the center; you can see this with show-generated-chunks debug option. (39x39 is only entity-generating chunks; the tile-generating chunk diameter is even larger, but I'm not sure by how much.) With each chunk being 32x32 tiles, assuming a player has stayed in a level for a while but didn't explore, this would be 1,557,504 tiles per layer. And if we want to get the 300% natural prod, we'd have around 11 layers for a total of 17,132,544 tiles.

Not only that the Lua code has to go though millions of iterations, Factorio also needs to construct these millions of tile objects and add them into a resizable data structure to be returned to the Lua code.

Is this loop really necessary? If so, can it be optimized (say, by sampling)?

21 days ago

That is to find if tiles have been deleted (e.g. when removing alien biomes mod).
Is the consistency check so long for existing games when the mod is added or always loading a game when mods changed?

21 days ago

I think the API allows me to only execute this block if tiles were really removed

21 days ago

Is the consistency check so long for existing games when the mod is added or always loading a game when mods changed?

on_configuration_changed "is called when the game version or any mod version changed, when any mod was added or removed, when a startup setting has changed, when any prototypes have been added or removed, or when a migration was applied." [1]

[1] https://lua-api.factorio.com/latest/classes/LuaBootstrap.html#on_configuration_changed

In our testing we were changing prototypes in a different mod.

21 days ago

That is to find if tiles have been deleted (e.g. when removing alien biomes mod).

Hmm, can this be postponed to when the tile is revealed (when mining the rocks)? If the tile has never been exposed then user doesn't really need to know what's originally there right?

21 days ago
(updated 21 days ago)

Hmm, can this be postponed to when the tile is revealed (when mining the rocks)? If the tile has never been exposed then user doesn't really need to know what's originally there right?

Don't think so, because then the mod would need calculate autoplace generation on every mining action which would also take it's time if there are many tiles. The current approach is easier: let the game generating all terrain upon chunk generation and then just hide the terrain tile under out-of-map until revealed. Calculating tile properties is quite expensive and should be done always for a big amount of positions

21 days ago
(updated 21 days ago)

the mod would need calculate autoplace generation on every mining action which would also take it's time if there are many tiles

You're not calculating autoplace in on_configuration_changed. The code in on_configuration_changed does a mechanical replacement from grass-1 to mineral-brown-dirt-2 if it exists, or grass-4.

Calculations on every mining action is already happening. You have this code:

function clear_subsurface(surface, pos, radius, clearing_radius, return_inventory)
[...]
    for x, y in iarea(area) do -- first, replace all out-of-map tiles with their hidden tile which means that it is inside map limits)
        if (x-pos.x)^2 + (y-pos.y)^2 < radius^2 and surface.get_hidden_tile({x, y}) then
            local wall = surface.find_entities_filtered{name = subsurface_wall_names, position = {x, y}}[1]
            if wall then
                wall.mine{inventory = return_inventory and inventory or nil, force = true, raise_destroyed = false, ignore_minable = true}
            end
            table.insert(new_tiles, {name = surface.get_hidden_tile({x, y}), position = {x, y}})
            surface.set_hidden_tile({x, y}, nil)
            table.insert(new_resource_positions, {x, y})
        end
    end

[...]

script.on_event({defines.events.on_player_mined_entity, defines.events.on_robot_mined_entity}, function(event)
    if event.entity.name == "surface-drill" then
        if event.entity.mining_target then event.entity.mining_target.destroy() end
    else
        for _, w in ipairs(subsurface_wall_names) do
            if event.entity.valid and event.entity.name == w then clear_subsurface(event.entity.surface, event.entity.position, 1.5) end
        end
    end
end)

The surface.get_hidden_tile({x, y}) can be compared to grass-1 and set to mineral-brown-dirt-2 / grass-4 when needed. This replicates the effect of on_configuration_changed without needing to calculate millions of tiles.

21 days ago

I'll look what I can do

20 days ago
(updated 20 days ago)

I added a check to only execute the code in on_configuration_changed if tiles have been removed, because never change a running system

New response