show-buildable/control.lua

changeset 14
c26d4dd2af9b
--- /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

mercurial