How to match blueprint entities to actual entities?

Place to get help with not working mods / modding interface.
Post Reply
mrvn
Smart Inserter
Smart Inserter
Posts: 5704
Joined: Mon Sep 05, 2016 9:10 am
Contact:

How to match blueprint entities to actual entities?

Post by mrvn »

I'm trying to use the new LuaItemStack.set_blueprint_entity_tag() function to save lua data from entities in a blueprint.

For that I catch the defines.events.on_player_setup_blueprint event which lets me iterate over the blueprinted entities and set tags.

The problem I face is that LuaItemStack.get_blueprint_entities() returns an array of dummy entities that will be included in the blueprint. It does not give the actual entities that are being blueprinted. I need the original entities to extract the lua data from them and store that into tags.

The event contains the area the blueprint is being taken of. So I have at least some idea where to look for the original entities. But how do I match them up? I tried player.surface.find_entities(event.area) but that doesn't really help as the blueprint will be cropped to fit the selected entities and doesn't match the dimension of the area of the event.

Attached a minimal example of taking a blueprint and adding tags with some debug logging. I always set the tag to 23 but, just as an example, I want to save the original entities unit_number.
Attachments
LogisticTrainOrganizer_0.0.0.zip
(14.86 KiB) Downloaded 66 times

User avatar
eradicator
Smart Inserter
Smart Inserter
Posts: 5206
Joined: Tue Jul 12, 2016 9:03 am
Contact:

Re: How to match blueprint entities to actual entities?

Post by eradicator »

Theorycrafting: The center of the blueprint should be at the center of the bounding box covering the outermost entities in the blueprint. So:

local t,b,l,r = 0,0,0,0
for entity in find_entities_filtered do if entity.pos.x > r then r = entity.pos.x
--etcpp for all four edges
local center = center({{t,l}{r,b}})

then you can substract the center from each entity's position and *should* have the same offset that the blueprint uses for each entity.

But there are edge cases, and official support would be nicer: viewtopic.php?f=28&t=75369
Author of: Belt Planner, Hand Crank Generator, Screenshot Maker, /sudo and more.
Mod support languages: 日本語, Deutsch, English
My code in the post above is dedicated to the public domain under CC0.

mrvn
Smart Inserter
Smart Inserter
Posts: 5704
Joined: Mon Sep 05, 2016 9:10 am
Contact:

Re: How to match blueprint entities to actual entities?

Post by mrvn »

eradicator wrote:
Tue Sep 10, 2019 4:14 pm
Theorycrafting: The center of the blueprint should be at the center of the bounding box covering the outermost entities in the blueprint. So:

local t,b,l,r = 0,0,0,0
for entity in find_entities_filtered do if entity.pos.x > r then r = entity.pos.x
--etcpp for all four edges
local center = center({{t,l}{r,b}})

then you can substract the center from each entity's position and *should* have the same offset that the blueprint uses for each entity.

But there are edge cases, and official support would be nicer: viewtopic.php?f=28&t=75369
Are you sure it's the position and not the collision box or selection box? If you don't know I will try all three tomorrow and see which one it is.

As for the corner cases of having entities with the same name at the same position I believe that is usually a non issue. If one of the entities is in the blueprint the other will be too. And as creator of those entities (who else would set a tag on them?) you should know how to handle that case. e.g. use the entities direction to separate them or simply tag them both.

User avatar
eradicator
Smart Inserter
Smart Inserter
Posts: 5206
Joined: Tue Jul 12, 2016 9:03 am
Contact:

Re: How to match blueprint entities to actual entities?

Post by eradicator »

mrvn wrote:
Wed Sep 11, 2019 1:02 am
Are you sure it's the position and not the collision box or selection box? If you don't know I will try all three tomorrow and see which one it is.
Absoluetely not. I just wrote down a theory about a rough approach. Haven't done any testing yet. But i think that the position is the most likely candidate.
mrvn wrote:
Wed Sep 11, 2019 1:02 am
As for the corner cases of having entities with the same name at the same position I believe that is usually a non issue.
It would only affect the author of those entities yes. If that's not you then lucky you ;).
Author of: Belt Planner, Hand Crank Generator, Screenshot Maker, /sudo and more.
Mod support languages: 日本語, Deutsch, English
My code in the post above is dedicated to the public domain under CC0.

mrvn
Smart Inserter
Smart Inserter
Posts: 5704
Joined: Mon Sep 05, 2016 9:10 am
Contact:

Re: How to match blueprint entities to actual entities?

Post by mrvn »

It looks like the bounding box is used. It's not the position for sure.

The center seems to be also aligned to the grid of the entities involved. That means usually in the center of a tile. Except rail road entities have a 2x2 grid and then it changes to the center of that.

Also ghosts seem to appear as full entities in the BP.

mrvn
Smart Inserter
Smart Inserter
Posts: 5704
Joined: Mon Sep 05, 2016 9:10 am
Contact:

Re: How to match blueprint entities to actual entities?

Post by mrvn »

Tiles further change the center of a blueprint. But I think I can handle all cases now. I made a small example mod. Feel free to use it in your own mods under whatever (sensible) license you wish:

https://mods.factorio.com/mod/example-entity-with-tags
https://github.com/mrvn/factorio-exampl ... -with-tags

User avatar
eradicator
Smart Inserter
Smart Inserter
Posts: 5206
Joined: Tue Jul 12, 2016 9:03 am
Contact:

Re: How to match blueprint entities to actual entities?

Post by eradicator »

Hmhm. So bounding boxes for entities, position for tiles, and a bit of magic to compensate for rail alignment? Only slightly more annoying than expected. I'm really hoping for official support to happen, and i really want a unit_number safe solution just so i don't have to bother thinking about edge-cases all the time.

Also control.lua states that it is GPL while the mod portal lists it as Unlicense. Also i've heared rumors that Unlicense might be ineffective under German law. #FunWithLicenses
Author of: Belt Planner, Hand Crank Generator, Screenshot Maker, /sudo and more.
Mod support languages: 日本語, Deutsch, English
My code in the post above is dedicated to the public domain under CC0.

mrvn
Smart Inserter
Smart Inserter
Posts: 5704
Joined: Mon Sep 05, 2016 9:10 am
Contact:

Re: How to match blueprint entities to actual entities?

Post by mrvn »

eradicator wrote:
Wed Sep 11, 2019 4:11 pm
Also control.lua states that it is GPL while the mod portal lists it as Unlicense. Also i've heared rumors that Unlicense might be ineffective under German law. #FunWithLicenses

Code: Select all

  * Permission granted to relicense when used in a larger project.
The mod portal doesn't have a choice for that and PD seems to cover it best. It's an example, I don't consider it copyrightable material as that would defeat the purpose of it being an example for others to use. Relicense it to whatever you need and then it's on your head.

Cobaltur
Inserter
Inserter
Posts: 47
Joined: Sat Sep 24, 2016 1:33 pm
Contact:

Re: How to match blueprint entities to actual entities?

Post by Cobaltur »

Hi mrvn,

you demo works very well (22 Download at the moment).

I got no larger project :D MathCoProcessor a combinator to store the functional behavior
I integrated it with minor so it can even reuse even more easily. It looks nice.

Here are the 3 ingredients if anyone is interested:

1) the callback to mrvn script to write the custom setting

Code: Select all


local blueprint = require "tag_support"

blueprint.blueprintCallback = function(bp, bp_entity, entity)
	if entity.name == "math-allInOne" then
		local myEntity = getEntity(entity)
		bp.set_blueprint_entity_tag(bp_entity.entity_number, "mathOp", myEntity.mathOp)
	end
end
2) a) read the blueprint on placing the entity

Code: Select all

local function onPlaceEntity(event)

		if event.tags and event.tags.mathOp then
			-- build by blueprint -> so copy data
			mathOp = event.tags.mathOp or mathOp
		end
		table.insert(...)
end

script.on_event(defines.events.on_built_entity, onPlaceEntity)
script.on_event(defines.events.on_robot_built_entity, onPlaceEntity)
my complete placing routine also handles my migration of deprecated recipes/items it deprecated blueprints.

Code: Select all

local function onPlaceEntity(event)
	-- we have deprecated items in chests and in blueprints
	-- sp this script will replace them by on demand
	local migratedEntity = Migration.migrateEntity(event.created_entity)
	if migratedEntity ~= nil then
		table.insert(global.math_combinators, migratedEntity)
	end

	if event.created_entity and event.created_entity.valid and includedEntityTypes[event.created_entity.name] then
		event.created_entity.rotatable = true

		local mathOp = "max" -- default, should not happens
		if event.tags and event.tags.mathOp then
			-- build by blueprint -> so copy data
			mathOp = event.tags.mathOp or mathOp
		end

		table.insert(global.math_combinators, {comb = event.created_entity, mathOp = mathOp})
	end
end
3) mrvn adaptod script "tag_support"

Code: Select all

--[[ example-entity-with-tags
  * Copyright (c) 2019 Goswin von Brederlow
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
  * the Free Software Foundation, either version 3 of the License, or
  * (at your option) any later version.
  *
  * This program is distributed in the hope that it will be useful,
  * but WITHOUT ANY WARRANTY; without even the implied warranty of
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  * GNU General Public License for more details.
  *
  * You should have received a copy of the GNU General Public License
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  *
  * Example code showing how to use entity-with-tags in blueprints.
  * 
  * Permission granted to relicense when used in a larger project.
--]]
-- adapted script to credit author and to easy reuse
-- there is a callback that needs to be installed
-- blueprintCallback = function (itemstack,bp_entity,entity)

local lib = {}

rail_types = {}
rail_types["straight-rail"] = true
rail_types["curved-rail"] = true
rail_types["rail-signal"] = true
rail_types["rail-chain-signal"] = true
rail_types["train-stop"] = true

local function de_ghost_entity(entity)
  local name = entity.name
  if (name == "entity-ghost") then
    return entity.ghost_name, entity.ghost_type
  else
    return name, entity.prototype.type
  end
end

function lib.player_setup_blueprint(event)
  -- log("player_setup_blueprint")
  -- log(serpent.block(event))
  local player = game.players[event.player_index]
  local itemstack = player.blueprint_to_setup
  if not itemstack or not itemstack.valid_for_read then
    itemstack = player.cursor_stack
  end
  if not itemstack or not itemstack.valid_for_read then
    return
  end
  local entities = itemstack.get_blueprint_entities()
  if not entities then
    return
  end
  -- map blueprint entities to real entities
  -- bounding box for all entities
  local included_entities = {}
  for _, entity in ipairs(entities) do
    included_entities[entity.name] = true
  end
  local map_entities = player.surface.find_entities(event.area)
  local min_x = 2147483647
  local min_y = 2147483647
  local max_x = -2147483648
  local max_y = -2147483648
  local has_rails = false
  for _, entity in ipairs(map_entities) do
    local name, proto_type = de_ghost_entity(entity)
    if (not event.alt) or included_entities[name] then
      if rail_types[proto_type] then
        has_rails = true
      end
      local box = entity.bounding_box
      if box.left_top.x < min_x then
        min_x = box.left_top.x
      end
      if box.left_top.y < min_y then
        min_y = box.left_top.y
      end
      if box.right_bottom.x > max_x then
        max_x = box.right_bottom.x
      end
      if box.right_bottom.y > max_y then
        max_y = box.right_bottom.y
      end
    end
  end
  -- bounding box for all tiles
  local tiles = itemstack.get_blueprint_tiles()
  local included_tiles = {}
  local map_tiles = {}
  if tiles then
    for _, tile in ipairs(tiles) do
      included_tiles[tile.name] = true
    end
    map_tiles = player.surface.find_tiles_filtered {area = event.area}
    for _, tile in ipairs(map_tiles) do
      local name = tile.name
      if tile.prototype.can_be_part_of_blueprint and tile.prototype.items_to_place_this then
        if name == "tile-ghost" then
          name = tile.prototype.name
        end
        if (not event.alt) or included_tile[name] then
          local pos = tile.position
          -- log("tile " .. name .. " at (" .. tile.position.x .. ", " .. tile.position.y .. ")")
          if pos.x < min_x then
            min_x = pos.x
          end
          if pos.y < min_y then
            min_y = pos.y
          end
          if pos.x + 1 > max_x then
            max_x = pos.x + 1
          end
          if pos.y + 1 > max_y then
            max_y = pos.y + 1
          end
        end
      end
    end
  end
  local cx, cy
  if has_rails then
    cx = math.floor((math.floor(min_x) + math.ceil(max_x)) / 4) * 2 + 1
    cy = math.floor((math.floor(min_y) + math.ceil(max_y)) / 4) * 2 + 1
  else
    cx = math.floor((math.floor(min_x) + math.ceil(max_x)) / 2) + 0.5
    cy = math.floor((math.floor(min_y) + math.ceil(max_y)) / 2) + 0.5
  end
  -- log("estimated blueprint center: (" .. min_x .. ", " .. min_y .. ")-(" .. max_x .. ", " .. max_y .. ") ==> (" .. cx .. ", " .. cy .. ")")
  -- match entities
  local cache = {}
  for i, entity in ipairs(entities) do
    -- log("bp entity " .. i .. ": " .. entity.name .. " at (" .. entity.position.x .. ", " .. entity.position.y .. ")")
    cache[entity.position.x .. "_" .. entity.position.y .. "_" .. entity.name] = entity
  end

  for i, entity in ipairs(map_entities) do
    local name, proto_type = de_ghost_entity(entity)
    if included_entities[name] then
      -- log("map entity " .. i .. ": " .. entity.name .. "[" .. name .. "]" .. " at (" .. entity.position.x - cx .. ", " .. entity.position.y - cy .. ")")
      local bp_entity = cache[entity.position.x - cx .. "_" .. entity.position.y - cy .. "_" .. name]

      if bp_entity then
        -- #moved to callback handler
        --     if entity.name == "math-allInOne" then
        --       -- set tag for our example tag-chest
        --       itemstack.set_blueprint_entity_tag(bp_entity.entity_number, "mathOp", "entity.unit_number")
        --     end
        lib.blueprintCallback(itemstack, bp_entity, entity)
      else
        log(
          "ERROR: map entity not found in blueprint: " ..
            entity.name ..
              "[" .. name .. "]" .. " at (" .. entity.position.x - cx .. ", " .. entity.position.y - cy .. ")"
        )
      end
    end
  end
end

-- function entity_built(event)
--   local entity = event.created_entity
--   -- log("entity_built: " .. entity.name)
--   if entity and entity.name == "tag-chest" then
--     -- log("  is tag-chest")
--     local tags = event.tags
--     if tags then
--       -- log("    with tags")
--       local data = tags["example-tag"]
--       if data then
--         -- log("      Entity build with example example-tag " .. data)
--       else
--         -- log("      example-tag missing")
--       end
--     else
--       -- log("    tags missing")
--     end
--   end
-- end

return lib


Post Reply

Return to “Modding help”