--- /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