show-buildable/control.lua

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
permissions
-rw-r--r--

Add show-buildable mod

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)

mercurial