-- Bounded room flood-fill. Revisions invalidate cached geometry on building edits. local C, M = core, hearth.model local ROOM_RADIUS, ROOM_BELOW, ROOM_ABOVE = 3, 1, 3 local MAX_CELLS = (ROOM_RADIUS * 2 + 1) ^ 2 * (ROOM_BELOW + ROOM_ABOVE + 1) local CACHE_SECONDS, CACHE_LIMIT = 1, 256 local cache, cache_count, revision = {}, 0, 0 local function key(pos) return math.floor(pos.x + 0.5) .. ":" .. math.floor(pos.y + 0.5) .. ":" .. math.floor(pos.z + 0.5) end local function read(pos) local node = C.get_node_or_nil(pos) if not node or node.name == "ignore" then return nil, nil, true end local def = C.registered_nodes[node.name] if not def then return node, nil, false end return node, def, false end local function room_geometry(pos) local head = vector.round({x = pos.x, y = pos.y + 1, z = pos.z}) local cache_key = key(head) local now = C.get_us_time() / 1000000 local old = cache[cache_key] if old and old.revision == revision and now - old.at < CACHE_SECONDS then return old.geometry end local minx, maxx = head.x - ROOM_RADIUS, head.x + ROOM_RADIUS local minz, maxz = head.z - ROOM_RADIUS, head.z + ROOM_RADIUS local miny, maxy = head.y - ROOM_BELOW, head.y + ROOM_ABOVE local first_node, first_def, unloaded = read(head) local geometry = {roof = false, shelter = 0, enclosed = false, ventilated = false, open_sides = 4, unloaded = unloaded} local function blocks_room(node, def) if not def then return true end -- Unknown nodes fail closed as room boundaries. if node.name == "hearth:door_open" then return false end return def.walkable ~= false end local function has_vent_near(p) for _, d in ipairs({{1,0,0},{-1,0,0},{0,1,0},{0,-1,0},{0,0,1},{0,0,-1}}) do local node, def, missing = read({x = p.x + d[1], y = p.y + d[2], z = p.z + d[3]}) unloaded = unloaded or missing if node and def and def.groups and (def.groups.hearth_vent or 0) > 0 then return true end end return false end -- Flood only passable nodes inside a fixed 7 x 7 x 5 volume. Reaching a -- horizontal boundary means that side admits weather; an upward/downward -- escape means the room lacks a ceiling/floor. This never searches map-scale. local queue, seen, index = {}, {}, 1 if first_node and first_def and not blocks_room(first_node, first_def) then queue[1] = head else geometry.unloaded = unloaded local roof = false for y = 1, 5 do local _, def, missing = read({x = head.x, y = head.y + y, z = head.z}) unloaded = unloaded or missing if def and def.walkable ~= false then roof = true; break end end geometry.roof, geometry.unloaded = roof, unloaded return geometry end local open_sides, ventilated = {}, false local directions = {{1,0,0},{-1,0,0},{0,1,0},{0,-1,0},{0,0,1},{0,0,-1}} while index <= #queue and index <= MAX_CELLS do local p = queue[index]; index = index + 1 local cell_key = key(p) if not seen[cell_key] then seen[cell_key] = true if p.x == minx then open_sides.west = true end if p.x == maxx then open_sides.east = true end if p.z == minz then open_sides.north = true end if p.z == maxz then open_sides.south = true end if p.y == maxy then open_sides.sky = true end if p.y == miny then open_sides.floor = true end ventilated = ventilated or has_vent_near(p) for _, d in ipairs(directions) do local npos = {x = p.x + d[1], y = p.y + d[2], z = p.z + d[3]} if npos.x >= minx and npos.x <= maxx and npos.z >= minz and npos.z <= maxz and npos.y >= miny and npos.y <= maxy then local nkey = key(npos) if not seen[nkey] then local node, def, missing = read(npos) unloaded = unloaded or missing if node and not blocks_room(node, def) then queue[#queue + 1] = npos end end end end end end if index > MAX_CELLS then open_sides.sky = true end local horizontal_open = (open_sides.west and 1 or 0) + (open_sides.east and 1 or 0) + (open_sides.north and 1 or 0) + (open_sides.south and 1 or 0) local roof = not open_sides.sky local enclosed = roof and not open_sides.floor and horizontal_open == 0 local roof_def for y = 1, 5 do local _, def, missing = read({x = head.x, y = head.y + y, z = head.z}) unloaded = unloaded or missing if def and def.walkable ~= false then roof_def = true; break end end roof = roof_def == true local closed_sides = 4 - horizontal_open local shelter = enclosed and roof and 1 or (roof and (0.25 + closed_sides * 0.1875) or 0) if unloaded then enclosed, shelter, ventilated = false, 0, true end geometry = {roof = roof, shelter = math.max(0, math.min(1, shelter)), enclosed = enclosed, ventilated = not enclosed or ventilated, open_sides = horizontal_open, unloaded = unloaded} if cache_count >= CACHE_LIMIT then cache, cache_count = {}, 0 end cache[cache_key] = {revision = revision, at = now, geometry = geometry} cache_count = cache_count + 1 return geometry end function hearth.invalidate_environment() revision = revision + 1 cache, cache_count = {}, 0 end function hearth.environment(pos) local room = room_geometry(pos) local fire = C.find_node_near(pos, 4, {"hearth:fire_lit"}) local heat = 0 if fire then local deadline = tonumber(C.get_meta(fire):get_string("fuel_until")) or 0 if M.fuel_remaining(deadline, os.time()) > 0 then local head = vector.round({x = pos.x, y = pos.y + 1, z = pos.z}) local from = vector.offset(fire, 0, 0.6, 0) if C.line_of_sight(from, head, 0.5) then heat = 1 - math.min(0.8, vector.distance(pos, fire) / 5) end end end return {roof = room.roof, shelter = room.shelter, enclosed = room.enclosed, ventilated = room.ventilated, open_sides = room.open_sides, heat = heat, smoke = heat > 0 and room.enclosed and not room.ventilated, rain = hearth.weather == "rain", temperature = hearth.clock.temperature - math.max(0, pos.y) * 0.0065} end