diff --git a/map_gen/maps/danger_ores/changelog.lua b/map_gen/maps/danger_ores/changelog.lua index 4ed6d502d..f4d966f49 100644 --- a/map_gen/maps/danger_ores/changelog.lua +++ b/map_gen/maps/danger_ores/changelog.lua @@ -160,4 +160,11 @@ return [[ - [DO:ScrapMaze] Added DO/Scrapworld-Maze preset - [DO:OmniMaze] Added DO/Omnimatter-Maze preset - [DO:CityBlocks] Added DO/City-Blocks preset + + 2026-07-29: + - [DO:CityBlocks] Halved main ore density + - [DO:CityBlocks] Left a 3 tile ore-free band inside each room along the rail corridors + - [DO:CityBlocks] Allowed roboports to be built on the rail corridors + - [DO] Build restrictions now apply to construction robots, not just player placement + - [DO] Fixed entities destroyed by a build restriction not being refunded ]] diff --git a/map_gen/maps/danger_ores/modules/city_blocks.lua b/map_gen/maps/danger_ores/modules/city_blocks.lua index 81cda2f48..d4fafd08f 100644 --- a/map_gen/maps/danger_ores/modules/city_blocks.lua +++ b/map_gen/maps/danger_ores/modules/city_blocks.lua @@ -2,9 +2,9 @@ -- lattice carrying a ready-made double-track network -- ordinary, minable rails: continuous -- lines ring every room and meet at a signalled RAIL ROUNDABOUT on every corner (geometry -- decoded from the user's blueprint; native Factorio 2.0 rail pieces). Players --- may branch their own rails, signals and poles anywhere on the lattice, but nothing else --- can be built there, and belts cannot span the 32-tile corridors: trains are the only --- inter-room logistics. Room ores are assigned once in on_init and stored in Global. +-- may branch their own rails, signals, poles and roboports anywhere on the lattice, but +-- nothing else can be built there, and belts cannot span the 32-tile corridors: trains are +-- the bulk inter-room logistics. Room ores are assigned once in on_init and stored in Global. local b = require 'map_gen.shared.builders' local Event = require 'utils.event' local Generate = require 'map_gen.shared.generate' @@ -20,6 +20,10 @@ local shuffle = table.shuffle_table local PITCH = 5 -- chunks per room+wall cell local ROOMS_RADIUS = 12 -- rooms span -R..R on both axes +-- Ore-free band inside each room along its edge with the corridor. Without it the ore starts +-- flush against the corridor and you have to hand-mine a bay before you can place a train stop +-- or an inserter, so nothing in a fresh room can be automated until it has been cleared by hand. +local ROOM_EDGE_MARGIN = 3 local RAIL_A = 13 -- track offsets within a corridor chunk: odd (chunk edges are multiples of local RAIL_B = 19 -- 32, matching the rail grid) and at corridor-center -3/+3, exactly where -- the corner roundabouts' approach lanes sit; the 4-tile gap still fits signals everywhere @@ -345,9 +349,9 @@ local function on_chunk(event) end end --- === strip placement rule ================================================== +-- === rail corridor placement rule ========================================== -local ALLOWED_ON_STRIP = { +local ALLOWED_ON_CORRIDOR = { ['straight-rail'] = true, ['curved-rail-a'] = true, ['curved-rail-b'] = true, @@ -358,37 +362,43 @@ local ALLOWED_ON_STRIP = { ['rail-chain-signal'] = true, ['train-stop'] = true, ['electric-pole'] = true, + -- Roboports are allowed even though bots can ferry items between rooms, because they can + -- do that already: a roboport's logistics_radius is 25 and roboports join into one network + -- when their logistic zones touch, so two placed inside adjacent rooms are within reach + -- across a 32 tile corridor without anything ever being built on the corridor itself. + -- Keeping them off it only forced players to give up the tidy placement, not the capability. + ['roboport'] = true, ['locomotive'] = true, ['cargo-wagon'] = true, ['fluid-wagon'] = true, ['artillery-wagon'] = true, } -local function on_wall_chunk(x, y) +local function on_corridor_chunk(x, y) local _, lx = room_of_chunk(floor(x / 32)) local _, ly = room_of_chunk(floor(y / 32)) return lx == PITCH - 1 or ly == PITCH - 1 end --- Corridor rule via the shared entity_placement_restriction module (handles ghosts, --- refunds and destruction): keep everything off the corridors, and on them only the --- allowed types (whitelisted by LuaEntity type, not name, for mod compatibility). +-- Corridor rule via the shared entity_placement_restriction module, which handles ghosts, +-- robot placement, refunds and destruction: keep everything off the corridors, and on them +-- only the allowed types (whitelisted by LuaEntity type, not name, for mod compatibility). local keep_alive_callback = Token.register(function(entity) local e_type = entity.type if e_type == 'entity-ghost' then e_type = entity.ghost_type end - if ALLOWED_ON_STRIP[e_type] then + if ALLOWED_ON_CORRIDOR[e_type] then return true end local pos = entity.position - return not on_wall_chunk(pos.x, pos.y) + return not on_corridor_chunk(pos.x, pos.y) end) local function on_restricted_destroyed(event) local player = event.player if player and player.valid then - player.print('Only rail infrastructure (rails, signals, stations, power poles) and trains can be built on the rail corridors!') + player.print('Only rail infrastructure (rails, signals, stations, power poles, roboports) and trains can be built on the rail corridors!') end end @@ -412,30 +422,59 @@ function Public.register(config) Event.add(RestrictEntities.events.on_restricted_entity_destroyed, on_restricted_destroyed) end +-- True for tiles in the ore-free band along a room's edge with the corridor. Rooms are +-- PITCH-1 chunks wide, so a room's outermost chunks are the ones at local offset 0 and +-- PITCH-2; only those can hold the band, and only on the side facing the corridor. +local function in_room_margin(x, y) + local tx, ty = floor(x), floor(y) + local _, lx = room_of_chunk(floor(tx / 32)) + local _, ly = room_of_chunk(floor(ty / 32)) + local ox, oy = tx % 32, ty % 32 + + if lx == 0 and ox < ROOM_EDGE_MARGIN then + return true + end + if lx == PITCH - 2 and ox >= 32 - ROOM_EDGE_MARGIN then + return true + end + if ly == 0 and oy < ROOM_EDGE_MARGIN then + return true + end + if ly == PITCH - 2 and oy >= 32 - ROOM_EDGE_MARGIN then + return true + end + return false +end + -- danger-ores main_ores_builder: per tile, pick the room's dominant-ore shape function Public.main_ores_builder(config) local main_ores = config.main_ores return function(tile_builder, ore_builder, spawn_shape, water_shape, _) local shapes = {} + -- Same ground, no ore entity: used for the room-edge band so the tiles still look like + -- the room's ore field but can be built on straight away. + local bare_shapes = {} for _, ore_data in ipairs(main_ores) do local land = tile_builder(ore_data.tiles) local ratios = ore_data.ratios local weighted = b.prepare_weighted_array(ratios) local ore = ore_builder(ore_data.name, ore_data.start, ratios, weighted) shapes[#shapes + 1] = b.apply_entity(land, ore) + bare_shapes[#bare_shapes + 1] = land end local function rooms(x, y, world) local ri = floor((floor(x / 32) + 2) / PITCH) local rj = floor((floor(y / 32) + 2) / PITCH) + local set = in_room_margin(x, y) and bare_shapes or shapes if ri == 0 and rj == 0 then -- spawn room splits the main ores across its quadrants local quadrant = ((x >= 0) and 1 or 0) + ((y >= 0) and 2 or 0) - return shapes[quadrant % #shapes + 1](x, y, world) + return set[quadrant % #set + 1](x, y, world) end local ore_index = data.room_ore[ri .. '/' .. rj] or 1 - return shapes[ore_index](x, y, world) + return set[ore_index](x, y, world) end return b.any { spawn_shape, water_shape, rooms } diff --git a/map_gen/maps/danger_ores/presets/danger_ore_city_blocks.lua b/map_gen/maps/danger_ores/presets/danger_ore_city_blocks.lua index a0ef95abf..5b7e5ecca 100644 --- a/map_gen/maps/danger_ores/presets/danger_ore_city_blocks.lua +++ b/map_gen/maps/danger_ores/presets/danger_ore_city_blocks.lua @@ -13,8 +13,8 @@ ScenarioInfo.add_map_extra_info([[ double-track network: continuous lines ring every room and meet at a signalled roundabout on every corner, and the rails are yours -- extend them, reroute them, mine them. Rails, signals, train stops, - power poles and trains are the only things buildable on the - corridors, and belts cannot cross them: trains are your logistics. + power poles, roboports and trains are the only things buildable on + the corridors, and belts cannot cross them: trains move the tonnage. ]]) local starting_items = Config.player_create.starting_items @@ -25,7 +25,9 @@ starting_items[#starting_items + 1] = { count = 1, name = 'locomotive' } starting_items[#starting_items + 1] = { count = 2, name = 'cargo-wagon' } starting_items[#starting_items + 1] = { count = 50, name = 'coal' } -local main_ores = require 'map_gen.maps.danger_ores.config.vanilla_ores' +-- Half the canonical density: a room is 4x4 chunks of solid single-ore field, so at full +-- richness one room outlasts anything you can build in it and clearing ground stops mattering. +local main_ores = require('map_gen.maps.danger_ores.config.vanilla_ores'):scale_richness(0.5) DOC.biter_drops.enabled = false DOC.scenario_name = 'danger-ore-city-blocks' diff --git a/map_gen/shared/entity_placement_restriction.lua b/map_gen/shared/entity_placement_restriction.lua index 6c0ac28d8..2621c19c3 100644 --- a/map_gen/shared/entity_placement_restriction.lua +++ b/map_gen/shared/entity_placement_restriction.lua @@ -12,7 +12,11 @@ This function must be a registered with the Token module and the keep_alive_callback function will take the Token-id as parameter This is to prevent upvalue errors - Refunds for items that were placed can be toggled on or off via the enable and disable_refund functions + Rules apply to both player and construction-robot placement, and to ghosts. + + Refunds for items that were placed can be toggled on or off via the enable and disable_refund functions. + The refunded item is derived from the entity's prototype (quality preserved) and goes back to + whoever built it: the placing player, or the construction robot for a robot build. Lastly, this module raises 2 events: on_pre_restricted_entity_destroyed and on_restricted_entity_destroyed events. They are fully defined below. @@ -171,7 +175,71 @@ local function entities_with_inventory(entity, player) return false end ---- Token for the on_built event callback, checks if an entity should be destroyed. +--- The item to hand back for a destroyed entity, quality preserved. +-- Derived from the entity prototype rather than the build event, because robot build events +-- carry no consumed_items at all. +local function placement_stack(entity) + local items = entity.prototype.items_to_place_this + if not items or #items == 0 then + return nil + end + local item = items[1] + if not item or not item.name then + return nil + end + return {name = item.name, count = item.count or 1, quality = entity.quality} +end + +--- Destroys a disallowed entity and refunds whoever built it (player or construction robot). +local function destroy_restricted(entity, ghost, event) + local index = event.player_index + local player = index and game.get_player(index) + local robot = event.robot + local stack = (not ghost) and placement_stack(entity) or nil + + raise_event( + Public.events.on_pre_restricted_entity_destroyed, + { + player_index = index, + created_entity = entity, + ghost = ghost, + stack = stack or {} + } + ) + + -- Need to revalidate the entity since we sent it to the raised event + if entity.valid then + -- Checking if the entity has an inventory and spills the content on the ground to prevent destroying those too + if player and player.valid and entities_with_inventory(entity, player) then + ghost = true -- Cheating to prevent refunds + else + entity.destroy() + end + end + + -- Refund to the robot when a robot built it, so the item returns to the logistic network + -- instead of vanishing; otherwise to the player who placed it. + local actor = robot or player + local item_returned = false + if stack and not ghost and primitives.refund and actor and actor.valid and actor.can_insert(stack) then + actor.insert(stack) + item_returned = true + end + + raise_event( + Public.events.on_restricted_entity_destroyed, + { + player_index = index, + player = player, + ghost = ghost, + item_returned = item_returned + } + ) +end + +--- Token for the build event callbacks, checks if an entity should be destroyed. +-- Registered for both on_built_entity and on_robot_built_entity: restricting only player +-- placement left every rule in this module bypassable with construction robots. local on_built_token = Token.register( function(event) @@ -212,73 +280,28 @@ local on_built_token = return end - local index = event.player_index - local stack = event.consumed_items.get_contents()[1] - if not stack then - if index then - return - else - stack = {} - end - end - - raise_event( - Public.events.on_pre_restricted_entity_destroyed, - { - player_index = index, - created_entity = entity, - ghost = ghost, - stack = stack - } - ) - - local player = game.get_player(index) - - -- Need to revalidate the entity since we sent it to the raised event - if entity.valid then - -- Checking if the entity has an inventory and spills the content on the ground to prevent destroying those too - if entities_with_inventory(entity, player) then - ghost = true -- Cheating to prevent refunds - else - entity.destroy() - end - end - - -- Check if we issue a refund: make sure refund is enabled, make sure we're not refunding a ghost, - -- and revalidate the stack since we sent it to the raised event - local item_returned - if player and player.valid and primitives.refund and not ghost and stack.valid then - player.insert(stack) - item_returned = true - else - item_returned = false - end - - raise_event( - Public.events.on_restricted_entity_destroyed, - { - player_index = index, - player = player, - ghost = ghost, - item_returned = item_returned - } - ) + destroy_restricted(entity, ghost, event) end ) ---- Registers and unregisters the event hook +--- Registers and unregisters the event hooks. +-- Both the player and the robot build event are hooked: a rule that only covers +-- on_built_entity is trivially bypassed by placing a ghost the bots then complete, or by any +-- ghost the game creates itself (entity death with a ghost, upgrade planner, undo). local function check_event_status() - -- First we check if the event hook is in place or not + -- First we check if the event hooks are in place or not if primitives.event then - -- If there are no items in either list and no function is present, unhook the event + -- If there are no items in either list and no function is present, unhook the events if not next(allowed_entities) and not next(banned_entities) and not primitives.keep_alive_callback then Event.remove_removable(defines.events.on_built_entity, on_built_token) + Event.remove_removable(defines.events.on_robot_built_entity, on_built_token) primitives.event = nil end else - -- If either of the lists have an entry or there is a function present, hook the event + -- If either of the lists have an entry or there is a function present, hook the events if next(allowed_entities) or next(banned_entities) or primitives.keep_alive_callback then Event.add_removable(defines.events.on_built_entity, on_built_token) + Event.add_removable(defines.events.on_robot_built_entity, on_built_token) primitives.event = true end end