Add show-buildable mod show-buildable_1.0.0

Sat, 05 Jul 2025 00:35:41 +0300

author
Teemu Piippo <teemu.s.piippo@gmail.com>
date
Sat, 05 Jul 2025 00:35:41 +0300
changeset 14
c26d4dd2af9b
parent 13
826df96c3720
child 15
9dba62aa8083

Add show-buildable mod

show-buildable/control.lua file | annotate | diff | comparison | revisions
show-buildable/data-final-fixes.lua file | annotate | diff | comparison | revisions
show-buildable/data-updates.lua file | annotate | diff | comparison | revisions
show-buildable/info.json file | annotate | diff | comparison | revisions
show-buildable/sb-util.lua file | annotate | diff | comparison | revisions
show-buildable/thumbnail.png file | annotate | diff | comparison | revisions
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/show-buildable/control.lua	Sat Jul 05 00:35:41 2025 +0300
@@ -0,0 +1,296 @@
+require"sb-util"
+
+---@param player LuaPlayer
+local function get_entity_player_wants_to_place(player)
+	local item_name = nil
+	if player.cursor_stack.valid_for_read
+	then
+		item_name = player.cursor_stack.name
+	elseif player.cursor_ghost
+	then
+		item_name = player.cursor_ghost.name.name
+	end
+	local entity_name = nil
+	local unsupported_types = {
+		["straight-rail"] = true,
+		["rail-ramp"] = true,
+		["rail-support"] = true,
+		["rail-signal"] = true,
+		["rail-chain-signal"] = true,
+		["train-stop"] = true,
+		["locomotive"] = true,
+		["cargo-wagon"] = true,
+		["fluid-wagon"] = true,
+		["offshore-pump"] = true,
+		["car"] = true,
+		["spider-vehicle"] = true,
+	}
+	if item_name
+	then
+		item_proto = prototypes.item[item_name]
+		if item_proto and item_proto.place_result
+			and not item_proto.place_result.flags["placeable-off-grid"]
+			and not unsupported_types[item_proto.place_result.type]
+		then
+			entity_name = item_proto.place_result.name
+		end
+	end
+	return entity_name
+end
+
+-- where to try place the entity inside the grid
+-- 1×1 entities require 0.5 offset, 2×2 require 1 offset,
+-- 3×3 entities require 0.5 again, 4×4 require 1.
+-- and so on...
+---@param collision_box data.BoundingBox
+local function offset_x(collision_box)
+	local x = collision_box.left_top.x
+	local oy = ((x%1) - (x % 0.5)) or 1
+	return oy > 0 and oy or 1
+end
+
+---@param collision_box data.BoundingBox
+local function offset_y(collision_box)
+	local y = collision_box.left_top.y
+	local oy = ((y%1) - (y % 0.5)) or 1
+	return oy > 0 and oy or 1
+end
+
+---@param proto LuaEntityPrototype
+--- can x be rotated???
+local function can_be_rotated(proto)
+	return proto.supports_direction and proto.collision_box.left_top.x ~= proto.collision_box.left_top.y
+end
+
+local function collision_tester(entity_name)
+	if entity_name and prototypes.entity["collision-tester-"..entity_name]
+	then
+		return "collision-tester-"..entity_name
+	else
+		return entity_name
+	end
+end
+
+---@param a table
+---@param b any
+local function find(a, b)
+	for k, v in pairs(a)
+	do
+		if v == b
+		then
+			return k
+		end
+	end
+end
+
+local function join(sep, x)
+	result = ""
+	local i = 0
+	for k, v in pairs(x)
+	do
+		if i > 0
+		then
+			result = result..sep
+		end
+		result = result..v
+		i = i + 1
+	end
+	return result
+end
+
+---@param a MapPosition
+---@param b MapPosition
+local function manh(a, b)
+	return math.abs(b.x - a.x) + math.abs(b.y - a.y)
+end
+
+---@param surface string
+---@param pos MapPosition
+---@param proto LuaEntityPrototype
+function resources_test(surface, pos, proto)
+	if storage.num_memoized_resource_tests == nil or storage.num_memoized_resource_tests > 1000
+	then
+		storage.num_memoized_resource_tests = 0
+		---@type table<string, table<number, table<number, table<string, boolean>>>>
+		storage.memoized_resource_tests = {}
+	end
+	if not storage.memoized_resource_tests[surface]
+	then
+		storage.memoized_resource_tests[surface] = {}
+	end
+	if not storage.memoized_resource_tests[surface][pos.x]
+	then
+		storage.memoized_resource_tests[surface][pos.x] = {}
+	end
+	if not storage.memoized_resource_tests[surface][pos.x][pos.y]
+	then
+		storage.memoized_resource_tests[surface][pos.x][pos.y] = {}
+	end
+	local arr = storage.memoized_resource_tests[surface][pos.x][pos.y]
+	if arr[proto.name] ~= nil
+	then
+		return arr[proto.name]
+	else
+		-- check resources
+		local resources = {}
+		for _, resource in pairs(prototypes.entity)
+		do
+			if resource.type == "resource"
+				and (
+					resource.resource_category == proto.resource_category
+					or (proto.resource_categories or {})[resource.resource_category]
+				)
+			then
+				table.insert(resources, resource.name)
+			end
+		end
+		local r = proto.mining_drill_radius
+		local resources_found = game.surfaces[surface].find_entities_filtered{
+			area = {{pos.x - r, pos.y - r}, {pos.x + r, pos.y + r}},
+			type = "resource",
+			name = resources,
+			limit = 1
+		}
+		-- special extra test for pumpjacks because those need to be placed
+		-- directly on top of the resource
+		local found = (#resources_found > 0)
+		if found and proto.mining_drill_radius < 1
+			and manh(resources_found[1].position, pos) > proto.mining_drill_radius
+		then
+			found = false
+		end
+		arr[proto.name] = found
+		return found
+	end
+end
+
+script.on_nth_tick(30, function()
+	for _, player in pairs(game.players)
+	do
+		if player.game_view_settings.show_entity_info
+		then
+			local entity_name = get_entity_player_wants_to_place(player)
+			local collision_box_table = {}
+			local scale = 1
+			if entity_name ~= nil
+			then
+				local proto = prototypes.entity[entity_name]
+				scale = 1
+				if proto.type == "cargo-bay" or proto.type == "cargo-landing-pad"
+				then
+					scale = 2
+				end
+				local scaled_ceil = function(x) return math.ceil(x / scale) * scale end
+				local rotated_box = {
+					left_top = {
+						x = proto.collision_box.left_top.y,
+						y = proto.collision_box.left_top.x,
+					},
+					right_bottom = {
+						x = proto.collision_box.right_bottom.y,
+						y = proto.collision_box.right_bottom.x,
+					},
+				}
+				---@param x number
+				---@param y number
+				---@param direction defines.direction
+				---@param collision_box data.BoundingBox
+				local function test_placement(x, y, direction, collision_box)
+					local pos = {
+						x = x + offset_x(collision_box) * scale,
+						y = y + offset_y(collision_box) * scale
+					}
+					local can_build = player.can_place_entity{
+						name = collision_tester(entity_name),
+						position = pos,
+						direction = direction,
+						terrain_building_size = 1,
+					}
+					if can_build and proto.type == "mining-drill" and not resources_test(player.surface.name, pos, proto)
+					then
+						can_build = false
+					end
+					if can_build
+					then
+						local xi = scaled_ceil(collision_box.left_top.x)
+						local xj = scaled_ceil(collision_box.right_bottom.x + 0.5*scale)
+						local yj = scaled_ceil(collision_box.right_bottom.y + 0.5*scale)
+						while xi < xj
+						do
+							local yi = scaled_ceil(collision_box.left_top.y)
+							while yi < yj
+							do
+								if not collision_box_table[x + xi]
+								then
+									collision_box_table[x + xi] = {}
+								end
+								collision_box_table[x + xi][y + yi] = true
+								yi = yi + scale
+							end
+							xi = xi + scale
+						end
+					else
+						if collision_box_table[x][y] == nil
+						then
+							collision_box_table[x][y] = false
+						end
+					end
+				end
+				local x0 = scaled_ceil(player.position.x)
+				local y0 = scaled_ceil(player.position.y)
+				local x = x0 - 12*scale
+				local xn = x0 + 11*scale
+				local yn = y0 + 11*scale
+				while x <= xn
+				do
+					if not collision_box_table[x]
+					then
+						collision_box_table[x] = {}
+					end
+					local y = y0 - 12*scale
+					while y <= yn
+					do
+						local collision_box = proto.collision_box
+						test_placement(x, y, defines.direction.north, collision_box)
+						if proto.type == "asteroid-collector"
+						then
+							test_placement(x, y, defines.direction.east, rotated_box)
+							test_placement(x, y, defines.direction.west, rotated_box)
+							test_placement(x, y, defines.direction.south, collision_box)
+						elseif can_be_rotated(proto)
+						then
+							test_placement(x, y, defines.direction.east, rotated_box)
+						end
+						y = y + scale
+					end
+					x = x + scale
+				end
+			end
+			local function red(a) return {r=a, g=0, b=0, a=a } end
+			local function green(a) return {r=0, g=a, b=0, a=a } end
+			for x, k in pairs(collision_box_table)
+			do
+				for y, can_build in pairs(k)
+				do
+					---@return LuaRendering.draw_rectangle_param
+					local parms = function(a)
+						return {
+							surface = player.surface,
+							color = (can_build and green or red)(a),
+							filled = true,
+							left_top = {x = x, y = y },
+							right_bottom = { x = x + scale - 0.05, y = y + scale - 0.05 },
+							time_to_live = 30,
+							players = {player},
+							only_in_alt_mode = true,
+						}
+					end
+					rendering.draw_rectangle(parms (0.2))
+					local parms_2 = parms(0.2)
+					parms_2.draw_on_ground = true
+					rendering.draw_rectangle(parms_2)
+				end
+			end
+		end
+	end
+end)
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/show-buildable/data-final-fixes.lua	Sat Jul 05 00:35:41 2025 +0300
@@ -0,0 +1,16 @@
+require"sb-util"
+
+-- in case something changes conditions or collision masks in the meantime
+for _, entity_type in pairs (entity_categories)
+do
+	for _, base_entity in pairs (data.raw[entity_type] or {})
+	do
+		local ct = data.raw["assembling-machine"]["collision-tester-"..base_entity.name]
+		if ct
+		then
+			ct.collision_mask = table.deepcopy(base_entity.collision_mask)
+			ct.collision_box = table.deepcopy(base_entity.collision_box)
+			ct.surface_conditions = table.deepcopy(base_entity.surface_conditions)
+		end
+	end
+end
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/show-buildable/data-updates.lua	Sat Jul 05 00:35:41 2025 +0300
@@ -0,0 +1,89 @@
+require("sb-util")
+
+---@param fluid_box data.FluidBox?
+local function mangle_fluid_box(fluid_box)
+	if fluid_box
+	then
+		for _, pipe_connection in pairs (fluid_box.pipe_connections)
+		do
+			pipe_connection.position = {0, 0}
+		end
+	end
+end
+
+local function supported(entity_type)
+	if (entity_type == "rail-support" or entity_type == "rail-ramp")
+		and not feature_flags.rail_bridges
+	then
+		return false
+	else
+		return true
+	end
+end
+
+-- Make a bunch of dummy entities for collision testing purposes
+local new_entities = {}
+for _, entity_type in pairs (entity_categories)
+do
+	for _, base_entity in pairs (supported(entity_type) and data.raw[entity_type] or {})
+	do
+		if has_flag(base_entity, "player-creation")
+		then
+			local new_entity = table.deepcopy (base_entity)
+			new_entity.name = "collision-tester-"..base_entity.name
+			new_entity.circuit_connector = nil
+			new_entity.next_upgrade = nil
+			new_entity.fast_replaceable_group = nil
+			mangle_fluid_box(new_entity.fluid_box)
+			for _, fluid_box in pairs (new_entity.fluid_boxes or {})
+			do
+				mangle_fluid_box(fluid_box)
+			end
+			mangle_fluid_box(new_entity.input_fluid_box)
+			mangle_fluid_box(new_entity.output_fluid_box)
+			mangle_fluid_box(new_entity.fuel_fluid_box)
+			mangle_fluid_box(new_entity.oxidizer_fluid_box)
+			if new_entity.type == "mining-drill"
+				or new_entity.type == "assembling-machine"
+				or new_entity.type == "fusion-generator"
+				or new_entity.type == "fusion-reactor"
+				or new_entity.type == "inserter"
+				or new_entity.type == "thruster"
+				or new_entity.type == "cargo-bay"
+				or new_entity.type == "cargo-landing-pad"
+				or new_entity.type == "transport-belt"
+				or new_entity.type == "splitter"
+				or new_entity.type == "container"
+				or new_entity.type == "loader"
+				or new_entity.type == "pump"
+				or new_entity.type == "rocket-silo"
+				or new_entity.type == "solar-panel"
+			then
+				-- I'd use simple entities here, but they need to be rotatable
+				new_entity.type = "assembling-machine"
+				new_entity.input_fluid_box = nil
+				new_entity.output_fluid_box = nil
+				new_entity.fluid_boxes = {}
+				new_entity.energy_usage = "1W"
+				new_entity.energy_source = {type="void"}
+				new_entity.crafting_speed = 1
+				new_entity.crafting_categories = {"crafting"}
+				new_entity.fixed_recipe = nil
+				new_entity.module_slots = 0
+			end
+			if (new_entity.type == "cargo-landing-pad" or new_entity.type == "cargo-bay")
+				and new_entity.graphics_set
+			then
+				new_entity.graphics_set.connections = nil
+			end
+			if (new_entity.type == "cargo-bay")
+				and new_entity.platform_graphics_set
+			then
+				new_entity.platform_graphics_set.connections = nil
+			end
+			table.insert (new_entities, new_entity)
+		end
+	end
+end
+
+data:extend (new_entities)
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/show-buildable/info.json	Sat Jul 05 00:35:41 2025 +0300
@@ -0,0 +1,8 @@
+{
+	"name": "show-buildable",
+	"author": "teemu",
+	"version": "1.0.0",
+	"factorio_version": "2.0",
+	"title": "Show buildable",
+	"description": "Shows where you can build your stuff. Experimental."
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/show-buildable/sb-util.lua	Sat Jul 05 00:35:41 2025 +0300
@@ -0,0 +1,117 @@
+entity_categories =
+{
+	"container",
+	"storage-tank",
+	"transport-belt",
+	"underground-belt",
+	"splitter",
+	"loader",
+	"inserter",
+	"electric-pole",
+	"pipe",
+	"pipe-to-ground",
+	"pump",
+	"straight-rail",
+	"half-diagonal-rail",
+	"curved-rail-a",
+	"curved-rail-b",
+	"elevated-straight-rail",
+	"elevated-half-diagonal-rail",
+	"elevated-curved-rail-a",
+	"elevated-curved-rail-b",
+	"legacy-straight-rail",
+	"legacy-curved-rail",
+	"rail-ramp",
+	"rail-support",
+	"train-stop",
+	"rail-signal",
+	"rail-chain-signal",
+	"logistic-container",
+	"roboport",
+	"lamp",
+	"arithmetic-combinator",
+	"decider-combinator",
+	"selector-combinator",
+	"constant-combinator",
+	"power-switch",
+	"programmable-speaker",
+	"display-panel",
+	"boiler",
+	"generator",
+	"fusion-reactor",
+	"fusion-generator",
+	"mining-drill",
+	"offshore-pump",
+	"furnace",
+	"assembling-machine",
+	"agricultural-tower",
+	"lab",
+	"lightning-attractor",
+	"reactor",
+	"beacon",
+	"rocket-silo",
+	"cargo-landing-pad",
+	"cargo-bay",
+	"asteroid-collector",
+	"thruster",
+	"wall",
+	"gate",
+	"radar",
+	"land-mine",
+	"ammo-turret",
+	"electric-turret",
+	"fluid-turret",
+	"artillery-turret",
+	"plant",
+	"simple-entity-with-force",
+	"simple-entity-with-owner",
+	"electric-energy-interface",
+	"linked-container",
+	"proxy-container",
+	"heat-interface",
+	"heat-pipe",
+	"lane-splitter",
+	"linked-belt",
+	"valve",
+	"burner-generator",
+	"cargo-pod",
+	"temporary-container",
+	"asteroid",
+	"turret",
+	"unit-spawner",
+	"spider-unit",
+	"character",
+	"simple-entity",
+	"corpse",
+	"rail-remnants",
+	"explosion",
+	"particle-source",
+	"fire",
+	"sticker",
+	"stream",
+	"artillery-flare",
+	"artillery-projectile",
+	"projectile",
+	"segment",
+	"spider-leg",
+	"beam",
+	"character-corpse",
+	"speech-bubble",
+	"loader-1x1",
+	"rocket-silo-rocket",
+	"market",
+	"solar-panel",
+}
+
+---@param entity data.EntityPrototype|LuaEntityPrototype
+---@param needle_flag EntityPrototypeFlag
+function has_flag(entity, needle_flag)
+	for _, flag in pairs(entity.flags or {})
+	do
+		if flag == needle_flag
+		then
+			return true
+		end
+	end
+	return false
+end
\ No newline at end of file
Binary file show-buildable/thumbnail.png has changed

mercurial