Sat, 05 Jul 2025 00:35:41 +0300
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)