# HG changeset patch # User Teemu Piippo # Date 1751664941 -10800 # Node ID c26d4dd2af9b1269be3f8e76f72216c2adea0df1 # Parent 826df96c3720c8544df56dde4abd32cc6bf2f859 Add show-buildable mod diff -r 826df96c3720 -r c26d4dd2af9b show-buildable/control.lua --- /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>>> + 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 diff -r 826df96c3720 -r c26d4dd2af9b show-buildable/data-final-fixes.lua --- /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 diff -r 826df96c3720 -r c26d4dd2af9b show-buildable/data-updates.lua --- /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 diff -r 826df96c3720 -r c26d4dd2af9b show-buildable/info.json --- /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 diff -r 826df96c3720 -r c26d4dd2af9b show-buildable/sb-util.lua --- /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 diff -r 826df96c3720 -r c26d4dd2af9b show-buildable/thumbnail.png Binary file show-buildable/thumbnail.png has changed