Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions map_gen/maps/danger_ores/changelog.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
]]
67 changes: 53 additions & 14 deletions map_gen/maps/danger_ores/modules/city_blocks.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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

Expand All @@ -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 }
Expand Down
8 changes: 5 additions & 3 deletions map_gen/maps/danger_ores/presets/danger_ore_city_blocks.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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'
Expand Down
137 changes: 80 additions & 57 deletions map_gen/shared/entity_placement_restriction.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
Loading