Первый фикс

Пачки некоторых позиций увеличены
This commit is contained in:
2024-03-01 20:53:32 +03:00
commit 7c9c708c92
23653 changed files with 767936 additions and 0 deletions

View File

@@ -0,0 +1,786 @@
--- Tools for working with bounding boxes.
-- @module Area.Area
-- @usage local Area = require('__stdlib__/stdlib/area/area')
-- @see Area.Position
-- @see Concepts.BoundingBox
-- @see Concepts.Position
local Area = { __class = 'Area', __index = require('__stdlib__/stdlib/core') }
setmetatable(Area, Area)
local Position = require('__stdlib__/stdlib/area/position')
local math = require('__stdlib__/stdlib/utils/math')
local string = require('__stdlib__/stdlib/utils/string')
local abs, floor, max = math.abs, math.floor, math.max
local metatable
--- Constructor Methods
-- @section Constructors
Area.__call = function(_, ...)
local type = type((...))
if type == 'table' then
local t = (...)
if t.left_top and t.right_bottom then
return Area.load(...)
else
return Area.new(...)
end
elseif type == 'string' then
return Area.from_string(...)
else
return Area.construct(...)
end
end
local function new_area(lt, rb, o)
return setmetatable({ left_top = lt, right_bottom = rb, orientation = o }, metatable)
end
--- Converts an area in either array or table format to an area with a metatable.
-- Returns itself if it already has a metatable
-- @tparam Concepts.BoundingBox area the area to convert
-- @treturn Concepts.BoundingBox a converted area
function Area.new(area)
local left_top = Position.new(area.left_top or area[1])
local right_bottom = Position.new(area.right_bottom or area[2] or area[1])
return setmetatable({ left_top = left_top, right_bottom = right_bottom, orientation = area.orientation }, metatable)
end
--- Creates an area from number parameters.
-- @tparam[opt=0] number x1 x-position of left_top, first position
-- @tparam[opt=0] number y1 y-position of left_top, first position
-- @tparam[opt=0] number x2 x-position of right_bottom, second position
-- @tparam[opt=0] number y2 y-position of right_bottom, second position
-- @treturn Concepts.BoundingBox the area in a table format
function Area.construct(...)
local args = type((...)) == 'table' and { select(2, ...) } or { select(1, ...) }
local lt = Position.construct_xy(args[1] or 0, args[2] or 0)
local rb = Position.construct_xy(args[3] or lt.x, args[4] or lt.y)
return setmetatable({ left_top = lt, right_bottom = rb }, metatable)
end
--- Loads the metatable into the passed Area without creating a new one.
-- @tparam Concepts.BoundingBox area the Area to set the metatable onto
-- @treturn Concepts.BoundingBox the Area with metatable attached
function Area.load(area)
area.left_top = Position.load(area.left_top)
area.right_bottom = Position.load(area.right_bottom)
return setmetatable(area, metatable)
end
--- Converts an area string to an area.
-- @tparam string area_string the area to convert
-- @treturn Concepts.BoundingBox
function Area.from_string(area_string)
return Area(load('return ' .. area_string)())
end
--- Converts a string key area to an area.
-- @tparam string area_string the area to convert
-- @treturn Concepts.BoundingBox
function Area.from_key(area_string)
local tab = string.split(area_string, ',', false, tonumber)
local lt = Position.new { x = tab[1], y = tab[2] }
local rb = Position.new { x = tab[3], y = tab[4] }
return new_area(lt, rb)
end
--- Area Methods
-- @section Methods
--- Stores the area for recall later, not deterministic.
-- Only the last area stored is saved.
-- @tparam Concepts.BoundingBox area
function Area.store(area)
rawset(getmetatable(area), '_saved', area)
return area
end
--- Recalls the stored area.
-- @tparam Concepts.BoundingBox area
-- @treturn Concepts.BoundingBox the stored area
function Area.recall(area)
return rawget(getmetatable(area), '_saved')
end
--- Normalizes the given area.
-- <ul>
-- <li>Swaps the values between `right_bottom.x` & `left_top.x` **IF** `right_bottom.x` < `left_top.x`
-- <li>Swaps the values between `right_bottom.y` & `left_top.y` **IF** `right_bottom.y` < `left_top.y`
-- </ul>
-- @tparam Concepts.BoundingBox area the area to normalize
-- @treturn Concepts.BoundingBox a new normalized area
function Area.normalize(area)
local left_top = Position.new(area.left_top)
local right_bottom = Position.new(area.right_bottom)
if right_bottom.x < left_top.x then left_top.x, right_bottom.x = right_bottom.x, left_top.x end
if right_bottom.y < left_top.y then left_top.y, right_bottom.y = right_bottom.y, left_top.y end
return new_area(left_top, right_bottom, area.orientation)
end
--- Normalize an area in place.
-- @tparam Concepts.BoundingBox area the area to normalize
-- @treturn area The area normalized in place
function Area.normalized(area)
local lt, rb = area.left_top, area.right_bottom
if rb.x < lt.x then lt.x, rb.x = rb.x, lt.x end
if rb.y < lt.y then lt.y, rb.y = rb.y, lt.y end
return area
end
--- Convert area from pixels.
-- @tparam Concepts.BoundingBox area
-- @treturn Concepts.BoundingBox
function Area.from_pixels(area)
return new_area(Position.from_pixels(area.left_top), Position.from_pixels(area.right_bottom), area.orientation)
end
--- Convert area to pixels.
-- @tparam Concepts.BoundingBox area
-- @treturn Concepts.BoundingBox
function Area.to_pixels(area)
return new_area(Position.to_pixels(area.left_top), Position.to_pixels(area.right_bottom), area.orientation)
end
--- Rounds an areas points to its closest integer.
-- @tparam Concepts.BoundingBox area
-- @treturn Concepts.BoundingBox
function Area.round(area)
return new_area(Position.round(area.left_top), Position.round(area.right_bottom), area.orientation)
end
--- Ceils an area by increasing the size of the area outwards
-- @tparam Concepts.BoundingBox area the area to round
-- @treturn Concepts.BoundingBox
function Area.ceil(area)
return new_area(Position.floor(area.left_top), Position.ceil(area.right_bottom), area.orientation)
end
--- Floors an area by decreasing the size of the area inwards.
-- @tparam Concepts.BoundingBox area the area to round
-- @treturn Concepts.BoundingBox
function Area.floor(area)
return new_area(Position.ceil(area.left_top), Position.floor(area.right_bottom), area.orientation)
end
-- When looking for tile center points, look inwards on right bottom
-- when x or y is int. This will keep the area with only the tiles it
-- contains.
local function right_bottom_center(pos)
local x, y
local fx, fy = floor(pos.x), floor(pos.y)
x = fx == pos.x and (fx - 0.5) or (fx + 0.5)
y = fy == pos.y and (fy - 0.5) or (fy + 0.5)
return Position.construct_xy(x, y)
end
--- Gets the center positions of the tiles where the given area's two positions reside.
-- @tparam Concepts.BoundingBox area
-- @treturn Concepts.BoundingBox the area with its two positions at the center of the tiles in which they reside
function Area.center_points(area)
return new_area(Position.center(area.left_top), right_bottom_center(area.right_bottom), area.orientation)
end
--- add left_bottom and right_top to the area
-- @tparam Concepts.BoundingBox area
-- @treturn Concepts.BoundingBox the area with left_bottom and right_top included
function Area.corners(area)
local lt, rb = area.left_top, area.right_bottom
local lb = area.left_bottom or Position.construct_xy(0, 0)
local rt = area.right_top or Position.construct_xy(0, 0)
lb.x, lb.y = lt.x, rb.y
rt.x, rt.y = rb.x, lt.y
area.left_bottom = lb
area.right_top = rt
return area
end
--- Flip an area such that the value of its width becomes its height, and the value of its height becomes its width.
-- @tparam Concepts.BoundingBox area the area to flip
-- @treturn Concepts.BoundingBox the fliped area
function Area.flip(area)
local w, h = Area.dimensions(area)
if w == h then
return area -- no point flipping a square
elseif h > w then
local rad = h / 2 - w / 2
return Area.adjust(area, { rad, -rad })
elseif w > h then
local rad = w / 2 - h / 2
return Area.adjust(area, { -rad, rad })
end
end
--- Return a non zero sized area by expanding if needed
-- @tparam Concepts.BoundingBox area the area to check
-- @tparam number|Concepts.Vector amount the amount to expand
-- @treturn Concepts.BoundingBox the area
function Area.non_zero(area, amount)
amount = amount or 0.01
return Area.size(area) == 0 and Area.expand(area, amount) or area
end
--- Returns the area to the diameter from left_top
-- @tparam Concepts.BoundingBox area
-- @tparam number diameter
-- @treturn Concepts.BoundingBox
function Area.to_diameter(area, diameter)
diameter = diameter or 0.1
return new_area(Position.new(area.left_top), Position.add(area.left_top + diameter))
end
--- Returns the smallest sized area.
-- @tparam Concepts.BoundingBox area
-- @tparam Concepts.BooundingBox area2
-- @treturn Concepts.BoundingBox the smallest area
function Area.min(area, area2)
return (Area.size(Area) <= Area.size(area2) and area) or area2
end
--- Returns the largest sized area.
-- @tparam Concepts.BoundingBox area
-- @tparam Concepts.BooundingBox area2
-- @treturn Concepts.BoundingBox the largest area
function Area.max(area, area2)
return (Area.size(area) >= Area.size(area2) and area) or area2
end
--- Shrinks the area inwards by the given amount.
-- The area shrinks inwards from top-left towards the bottom-right, and from bottom-right towards the top-left.
-- @tparam Concepts.BoundingBox area the area to shrink
-- @tparam number|Concepts.Vector amount the amount to shrink
-- @treturn Concepts.BoundingBox the area reduced by amount
function Area.shrink(area, amount)
return new_area(Position.add(area.left_top, amount), Position.subtract(area.right_bottom, amount))
end
--- Expands the area outwards by the given amount.
-- @tparam Concepts.BoundingBox area the area
-- @tparam number|Concepts.Vector amount to expand each edge of the area outwards by
-- @treturn Concepts.BoundingBox the area expanded by amount
-- @see Area.shrink
function Area.expand(area, amount)
return new_area(Position.subtract(area.left_top, amount), Position.add(area.right_bottom, amount))
end
--- Adjust an area by shrinking or expanding.
-- Imagine pinching & holding with fingers the top-left & bottom-right corners of a 2D box and pulling outwards to expand and pushing inwards to shrink the box.
-- @usage local area = Area.adjust({{-2, -2}, {2, 2}}, {4, -1})
-- -- returns {left_top = {x = -6, y = -1}, right_bottom = {x = 6, y = 1}}
-- @tparam Concepts.BoundingBox area the area to adjust
-- @tparam number|Concepts.Vector amount the vectors to use
-- @treturn Concepts.BoundingBox the adjusted bounding box
function Area.adjust(area, amount)
local vec = Position(amount)
area = Area.new(area)
-- shrink or expand on x vector
if vec.x > 0 then
area = Area.expand(area, { vec.x, 0 })
elseif vec.x < 0 then
area = Area.shrink(area, { abs(vec.x), 0 })
end
-- shrink or expand on y vector
if vec.y > 0 then
area = Area.expand(area, { 0, vec.y })
elseif vec.y < 0 then
area = Area.shrink(area, { 0, abs(vec.y) })
end
return area
end
--- Offsets the area by the `{x, y}` values.
-- @tparam Concepts.BoundingBox area the area to offset
-- @tparam Concepts.Position pos the position to which the area will offset
-- @treturn Concepts.BoundingBox the area offset by the position
function Area.offset(area, pos)
local vec = Position(pos)
return new_area(Position.add(area.left_top, vec), Position.add(area.right_bottom, vec))
end
--- Translates an area in the given direction.
-- @tparam Concepts.BoundingBox area the area to translate
-- @tparam defines.direction direction the direction of translation
-- @tparam number distance the distance of the translation
-- @treturn Concepts.BoundingBox the area translated
function Area.translate(area, direction, distance)
direction = direction or 0
distance = distance or 1
return new_area(Position.translate(area.left_top, direction, distance),
Position.translate(area.right_bottom, direction, distance))
end
--- Set an area to the whole size of the surface.
-- @tparam Concepts.BoundingBox area
-- @tparam LuaSurface surface
-- @treturn Concepts.BoundingBox
function Area.to_surface_size(area, surface)
local w, h = surface.map_gen_settings.width, surface.map_gen_settings.height
area.left_top.x = -(w / 2)
area.right_bottom.x = (w / 2)
area.left_top.y = -(h / 2)
area.right_bottom.y = (h / 2)
return area
end
--- Shrinks an area to the size of the surface if it is bigger.
-- @tparam Concepts.BoundingBox area
-- @tparam LuaSurface surface
-- @treturn Concepts.BoundingBox
function Area.shrink_to_surface_size(area, surface)
local w, h = surface.map_gen_settings.width, surface.map_gen_settings.height
if abs(area.left_top.x) > w / 2 then
area.left_top.x = -(w / 2)
area.right_bottom.x = (w / 2)
end
if abs(area.left_top.y) > w / 2 then
area.left_top.y = -(h / 2)
area.right_bottom.y = (h / 2)
end
return area
end
--- Return the chunk coordinates from an area.
-- @tparam Concepts.BoundingBox area
-- @treturn Concepts.BoundingBox Chunk position coordinates
function Area.to_chunk_coords(area)
return Area.load {
left_top = { x = floor(area.left_top.x / 32), y = floor(area.left_top.y / 32) },
right_bottom = { x = floor(area.right_bottom.x / 32), y = floor(area.right_bottom.y / 32) }
}
end
--- Position Conversion Functions
-- @section ConversionFunctions
--- Calculates the center of the area and returns the position.
-- @tparam Concepts.BoundingBox area the area
-- @treturn Concepts.Position the center of the area
function Area.center(area)
local dist_x = area.right_bottom.x - area.left_top.x
local dist_y = area.right_bottom.y - area.left_top.y
return Position.construct_xy(area.left_top.x + (dist_x / 2), area.left_top.y + (dist_y / 2))
end
--- Area Functions
-- @section Functions
--- Return a suitable string for using as a table key
-- @tparam Concepts.BoundingBox area
-- @return string
function Area.to_key(area)
return table.concat({ area.left_top.x, area.left_top.y, area.right_bottom.x, area.right_bottom.y }, ',')
end
--- Converts an area to a string.
-- @tparam Concepts.BoundingBox area the area to convert
-- @treturn string the string representation of the area
function Area.to_string(area)
local left_top = 'left_top = ' .. area.left_top
local right_bottom = 'right_bottom = ' .. area.right_bottom
local orientation = area.orientation and ', ' .. area.orientation or ''
return '{' .. left_top .. ', ' .. right_bottom .. orientation .. '}'
end
--- Converts an area to an ltx, lty / rbx, rby string.
-- @tparam Concepts.BoundingBox area the area to convert
-- @treturn string the string representation of the area
function Area.to_string_xy(area)
return table.concat(area.left_top, ', ') .. ' / ' .. table.concat(area.right_bottom, ', ')
end
--- Is this a non zero sized area
-- @tparam Concepts.BoundingBox area
-- @treturn boolean
function Area.is_zero(area)
return Area.size(area) == 0
end
--- Is the area normalized.
-- @tparam Concepts.BoundingBox area
-- @treturn boolean
function Area.is_normalized(area)
return area.right_bottom.x >= area.left_top.x and area.right_bottom.y >= area.left_top.y
end
--- Is the area non-zero and normalized.
-- @tparam Concepts.BoundingBox area
-- @treturn boolean
function Area.valid(area)
return Area.is_normalized(area) and Area.size(area) ~= 0
end
--- Is this a simple area. {{num, num}, {num, num}}
-- @tparam Concepts.BoundingBox area
-- @treturn boolean
function Area.is_simple_area(area)
return Position.is_simple_position(area[1]) and Position.is_simple_position(area[2])
end
--- Is this a complex area {left_top = {x = num, y = num}, right_bottom = {x = num, y = num}}
-- @tparam Concepts.BoundingBox area
-- @treturn boolean
function Area.is_complex_area(area)
return Position.is_complex_position(area.left_top) and Position.is_complex_position(area.right_bottom)
end
--- Is this and area of any kind.
-- @tparam Concepts.BoundingBox area
-- @treturn boolean
function Area.is_area(area)
return Area.is_Area(area) or Area.is_complex_area(area) or Area.is_simple_area(area)
end
--- Does the area have the class attached
-- @tparam Concepts.BoundingBox area
-- @treturn boolean
function Area.is_Area(area)
return getmetatable(area) == metatable
end
--- Unpack an area into a tuple.
-- @tparam Concepts.Boundingbox area
-- @treturn tuple lt.x, lt.y, rb.x, rb.y
function Area.unpack(area)
return area.left_top.x, area.left_top.y, area.right_bottom.x, area.right_bottom.y, area.orientation
end
--- Unpack an area into a tuple of position tables.
-- @tparam Concepts.BoundingBox area
-- @treturn Concepts.Position left_top
-- @treturn Concepts.Position right_bottom
function Area.unpack_positions(area)
return area.left_top, area.right_bottom
end
--- Pack an area into an array.
-- @tparam Concepts.BoundingBox area
-- @treturn array
function Area.pack(area)
return { area.left_top.x, area.left_top.y, area.right_bottom.x, area.right_bottom.y, area.orientation }
end
--- Pack an area into a simple bounding box array
-- @tparam Concepts.BoundingBox area
-- @treturn Concepts.BoundingBox simple array
function Area.pack_positions(area)
return { { area.left_top.x, area.left_top.y }, { area.right_bottom.x, area.right_bottom.y } }
end
--- Gets the properties of the given area.
-- This function returns a total of four values that represent the properties of the given area.
-- @tparam Concepts.BoundingBox area the area from which to get the size
-- @treturn number the size of the area &mdash; (width &times; height)
-- @treturn number the width of the area
-- @treturn number the height of the area
-- @treturn number the perimeter of the area &mdash; (2 &times; (width + height))
function Area.size(area)
local width = Area.width(area)
local height = Area.height(area)
local area_size = width * height
local perimeter = (width + width) * 2
return area_size, width, height, perimeter
end
--- Return the rectangle.
-- @tparam Concepts.BoundingBox area
-- @treturn number left_top.x
-- @treturn number left_top.y
-- @treturn number width
-- @treturn number height
function Area.rectangle(area)
return area.left_top.x, area.left_top.y, Area.width(area), Area.height(area)
end
--- The width of the area.
-- @tparam Concepts.BoundingBox area
-- @treturn number width
function Area.width(area)
return abs(area.left_top.x - area.right_bottom.x)
end
--- The height of an area.
-- @tparam Concepts.BoundingBox area
-- @treturn number width
function Area.height(area)
return abs(area.left_top.y - area.right_bottom.y)
end
--- The dimensions of an area.
-- @tparam Concepts.BoundingBox area
-- @treturn number width
-- @treturn number height
function Area.dimensions(area)
return Area.width(area), Area.height(area)
end
--- The Perimiter of an area.
-- @tparam Concepts.BoundingBox area
-- @treturn number width
function Area.perimeter(area)
return (Area.width(area) + Area.height(area)) * 2
end
--- Returns true if two areas are the same.
-- @tparam Concepts.BoundingBox area1
-- @tparam Concepts.BoundingBox area2
-- @treturn boolean true if areas are the same
function Area.equals(area1, area2)
if not (area1 and area2) then return false end
local ori = area1.orientation or 0 == area2.orientation or 0
return ori and area1.left_top == area2.left_top and area1.right_bottom == area2.right_bottom
end
--- Is area1 smaller in size than area2
-- @tparam Concepts.BoundingBox area1
-- @tparam Concepts.BoundingBox area2
-- @treturn boolean is area1 less than area2 in size
function Area.less_than(area1, area2)
if type(area1) == 'number' then
return area1 < Area.size(area2)
elseif type(area2) == 'number' then
return Area.size(area1) < area2
else
return Area.size(area1) < Area.size(area2)
end
end
--- Is area1 smaller or equal in size to area2.
-- @tparam Concepts.BoundingBox area1
-- @tparam Concepts.BoundingBox area2
-- @treturn boolean is area1 less than or equal to area2 in size
-- @local
function Area.less_than_eq(area1, area2)
if type(area1) == 'number' then
return area1 <= Area.size(area2)
elseif type(area2) == 'number' then
return Area.size(area1) <= area2
else
return Area.size(area1) <= Area.size(area2)
end
end
--- Does either area overlap/collide with the other area.
-- @tparam Concepts.BoundingBox area1
-- @tparam Concepts.BoundingBox area2
-- @treturn boolean
function Area.collides(area1, area2)
local x1, y1 = Position.unpack(area1.left_top)
local _, w1, h1 = Area.size(area1)
local x2, y2 = Position.unpack(area2.left_top)
local _, w2, h2 = Area.size(area2)
return not ((x1 > x2 + w2) or (x1 > y2 + h2) or (x2 > x1 + w1) or (y2 > y1 + h1))
end
--- Are the passed positions all located in an area.
-- @tparam Concepts.BoundingBox area the search area
-- @tparam array positions array of Concepts.Position
-- @treturn boolean true if the positions are located in the area
function Area.contains_positions(area, positions)
for _, pos in pairs(positions) do if not Position.inside(pos, area) then return false end end
return true
end
--- Are all passed areas completly inside an area.
-- @tparam Concepts.BoundingBox area
-- @tparam array areas array of Concepts.BoundingBox
-- @treturn boolean
function Area.contains_areas(area, areas)
for _, inner in pairs(areas) do
if not Area.contains_positions(area, { Area.unpack_positions(inner) }) then return false end
end
return true
end
--- Do all passed areas collide with an area.
-- @tparam Concepts.BoundingBox area
-- @tparam array areas array of Concepts.BoundingBox
-- @treturn boolean
function Area.collides_areas(area, areas)
for _, inner in pairs(areas) do if not Area.collides(area, inner) then return false end end
return true
end
--- Area Iterators
-- @section Area Iterators
--- Iterates an area.
-- @usage
-- local area = {{0, -5}, {3, -3}}
-- for x,y in Area.iterate(area) do
-- -- return x, y values
-- end
-- for position in Area.iterate(area, true) do
-- -- returns a position object
-- end
-- -- Iterates from left_top.x to right_bottom.x then goes down y until right_bottom.y
-- @tparam Concepts.BoundingBox area the area to iterate
-- @tparam[opt=false] boolean as_position return a position object
-- @tparam[opt=false] boolean inside only return values that contain the areas tiles
-- @tparam[opt=1] number step size to increment
-- @treturn function an iterator
function Area.iterate(area, as_position, inside, step)
step = step or 1
local x, y = area.left_top.x, area.left_top.y
local max_x = area.right_bottom.x - (inside and 0.001 or 0)
local max_y = area.right_bottom.y - (inside and 0.001 or 0)
local first = true
local function iterator()
if first then
first = false
elseif x <= max_x and x + step <= max_x then
x = x + step
elseif y <= max_y and y + step <= max_y then
x = area.left_top.x
y = y + step
else
return
end
return (as_position and Position.construct_xy(x, y)) or x, (not as_position and y) or nil
end
return iterator
end
--- Iterates the given area in a spiral as depicted below, from innermost to the outermost location.
-- <p>![](http://i.imgur.com/EwfO0Es.png)
-- @usage for x, y in Area.spiral_iterate({{-2, -1}, {2, 1}}) do
-- print('(' .. x .. ', ' .. y .. ')')
-- end
-- prints: (0, 0) (1, 0) (1, 1) (0, 1) (-1, 1) (-1, 0) (-1, -1) (0, -1) (1, -1) (2, -1) (2, 0) (2, 1) (-2, 1) (-2, 0) (-2, -1)
-- @tparam Concepts.BoundingBox area the area on which to perform a spiral iteration
-- @tparam boolean as_position return a position object instead of x, y
-- @treturn function an iterator
function Area.spiral_iterate(area, as_position)
local rx = area.right_bottom.x - area.left_top.x + 1
local ry = area.right_bottom.y - area.left_top.y + 1
local half_x = floor(rx / 2)
local half_y = floor(ry / 2)
local center_x = area.left_top.x + half_x
local center_y = area.left_top.y + half_y
local size = max(rx, ry) ^ 2
local x, y, dx, dy = 0, 0, 0, -1
local positions = {}
local index = 1
for _ = 1, size do
if -(half_x) <= x and x <= half_x and -(half_y) <= y and y <= half_y then
positions[#positions + 1] = { x = x, y = y }
end
if x == y or (x < 0 and x == -y) or (x > 0 and x == 1 - y) then
local temp = dx
dx = -(dy)
dy = temp
end
x = x + dx
y = y + dy
end
local function iterator()
if index > #positions then return end
local pos = positions[index]
index = index + 1
pos.x = pos.x + center_x
pos.y = pos.y + center_y
return (as_position and Position.load(pos)) or pos.x, (not as_position and pos.y) or nil
end
return iterator, area, 0
end
--- Area Arrays
-- @section Area Arrays
function Area.positions(area, inside, step)
local positions = {}
for pos in Area.iterate(area, true, inside, step) do positions[#positions + 1] = pos end
return positions
end
--- Metamethods
-- @section Metamethods
local function __add(area1, area2)
area1, area2 = Area(area1), Area(area2)
area1.left_top = area1.left_top + area2.left_top
area1.right_bottom = area1.right_bottom + area2.right_bottom
return area1
end
local function __sub(area1, area2)
area1, area2 = Area(area1), Area(area2)
area1.left_top = area1.left_top - area2.left_top
area1.right_bottom = area1.right_bottom - area2.right_bottom
return area1
end
local function __mul(area1, area2)
area1, area2 = Area(area1), Area(area2)
area1.left_top = area1.left_top * area2.left_top
area1.right_bottom = area1.right_bottom * area2.right_bottom
return area1
end
local function __div(area1, area2)
area1, area2 = Area(area1), Area(area2)
area1.left_top = area1.left_top / area2.left_top
area1.right_bottom = area1.right_bottom / area2.right_bottom
return area1
end
local function __mod(area1, area2)
area1, area2 = Area(area1), Area(area2)
area1.left_top = area1.left_top % area2.left_top
area1.right_bottom = area1.right_bottom % area2.right_bottom
return area1
end
local function __unm(area)
---@diagnostic disable: assign-type-mismatch
area = Area.new(area)
area.left_top = -area.left_top
area.right_bottom = -area.right_bottom
---@diagnostic enable: assign-type-mismatch
return area
end
--- Area tables are returned with these Metamethods attached.
-- @table Metamethods
metatable = {
__class = 'area',
__index = Area, -- If key is not found see if there is one available in the Area module.
__tostring = Area.to_string, -- Will print a string representation of the area.
__concat = _ENV.concat, -- calls tostring on both sides of concat.
__add = __add, -- Will adjust if RHS is vector/position, add offset if RHS is number/area
__sub = __sub, -- Will adjust if RHS is vector/position, sub offset if RHS is number/area
__mul = __mul,
__div = __div,
__mod = __mod,
__unm = __unm,
__eq = Area.equals, -- Is area1 the same as area2.
__lt = Area.less_than, -- Is the size of area1 less than number/area2.
__le = Area.less_than_eq, -- Is the size of area1 less than or equal to number/area2.
__len = Area.size, -- The size of the area.
__call = Area.new, -- Return a new copy.
__debugline = [[<Area>{[}left_top={left_top},right_bottom={right_bottom}{]}]]
}
return Area

View File

@@ -0,0 +1,85 @@
--- For working with chunks.
-- A chunk represents a 32 tile<sup>2</sup> on a surface in Factorio.
-- @module Area.Chunk
-- @usage local Chunk = require('__stdlib__/stdlib/area/chunk')
-- @see Concepts.ChunkPosition
local Chunk = {
__class = 'Chunk',
__index = require('__stdlib__/stdlib/core')
}
setmetatable(Chunk, Chunk)
local Game = require('__stdlib__/stdlib/game')
local Position = require('__stdlib__/stdlib/area/position')
local AREA_PATH = '__stdlib__/stdlib/area/area'
Chunk.__call = Position.__call
--- Gets the chunk position of a chunk where the specified position resides.
-- @function Chunk.from_position
-- @see Area.Position.to_chunk_position
Chunk.from_position = Position.to_chunk_position
--- Gets the top_left position from a chunk position.
-- @function Chunk.to_position
-- @see Area.Position.from_chunk_position
Chunk.to_position = Position.from_chunk_position
--Chunk.to_center_position
--Chunk.to_center_tile_position
-- Hackish function, Factorio lua doesn't allow require inside functions because...
local function load_area(area)
local Area = package.loaded[AREA_PATH]
if not Area then
local log = log or function(_msg_) end
log('WARNING: Area for Position not found in package.loaded')
end
return Area and Area.load(area) or area
end
--- Gets the area of a chunk from the specified chunk position.
-- @tparam Concepts.ChunkPosition pos the chunk position
-- @treturn Concepts.BoundingBox the chunk's area
function Chunk.to_area(pos)
local left_top = Chunk.to_position(pos)
local right_bottom = Position.add(left_top, 32, 32)
return load_area { left_top = left_top, right_bottom = right_bottom }
end
--- Gets the user data that is associated with a chunk.
-- The user data is stored in the global object and it persists between loads.
-- @tparam LuaSurface surface the surface on which the user data is looked up
-- @tparam Concepts.ChunkPosition chunk_pos the chunk position on which the user data is looked up
-- @tparam[opt] Mixed default_value the user data to set for the chunk and returned if the chunk had no user data
-- @treturn ?|nil|Mixed the user data **OR** *nil* if it does not exist for the chunk and if no default_value was set
function Chunk.get_data(surface, chunk_pos, default_value)
surface = Game.get_surface(surface)
assert(surface, 'invalid surface')
local key = Position(chunk_pos):to_key()
return Game.get_or_set_data('_chunk_data', surface.index, key, false, default_value)
end
Chunk.get = Chunk.get_data
--- Associates the user data to a chunk.
-- The user data will be stored in the global object and it will persist between loads.
-- @tparam LuaSurface surface the surface on which the user data will reside
-- @tparam Concepts.ChunkPosition chunk_pos the chunk position to associate with the user data
-- @tparam ?|nil|Mixed value the user data to set **OR** *nil* to erase the existing user data for the chunk
-- @treturn ?|nil|Mixed the previous user data associated with the chunk **OR** *nil* if the chunk had no previous user data
function Chunk.set_data(surface, chunk_pos, value)
surface = Game.get_surface(surface)
assert(surface, 'invalid surface')
local key = Position(chunk_pos):to_key()
return Game.get_or_set_data('_chunk_data', surface.index, key, true, value)
end
Chunk.set = Chunk.set_data
return Chunk

View File

@@ -0,0 +1,108 @@
--- Functions for working with directions and orientations.
-- @module Area.Direction
-- @usage local Direction = require('__stdlib__/stdlib/area/direction')
-- @see defines.direction
local Direction = {
__class = 'Direction',
__index = require('__stdlib__/stdlib/core')
}
setmetatable(Direction, Direction)
--- defines.direction.north
Direction.north = defines.direction.north
--- defines.direction.east
Direction.east = defines.direction.east
--- defines.direction.west
Direction.west = defines.direction.west
--- defines.direction.south
Direction.south = defines.direction.south
--- defines.direction.northeast
Direction.northeast = defines.direction.northeast
--- defines.direction.northwest
Direction.northwest = defines.direction.northwest
--- defines.direction.southeast
Direction.southeast = defines.direction.southeast
--- defines.direction.southwest
Direction.southwest = defines.direction.southwest
--- Returns the opposite direction
-- @tparam defines.direction direction the direction
-- @treturn defines.direction the opposite direction
function Direction.opposite(direction)
return (direction + 4) % 8
end
--- Returns the next direction.
--> For entities that only support two directions, see @{opposite}.
-- @tparam defines.direction direction the starting direction
-- @tparam[opt=false] boolean eight_way true to get the next direction in 8-way (note: not many prototypes support 8-way)
-- @treturn defines.direction the next direction
function Direction.next(direction, eight_way)
return (direction + (eight_way and 1 or 2)) % 8
end
--- Returns the previous direction.
--> For entities that only support two directions, see @{opposite}.
-- @tparam defines.direction direction the starting direction
-- @tparam[opt=false] boolean eight_way true to get the previous direction in 8-way (note: not many prototypes support 8-way)
-- @treturn defines.direction the next direction
function Direction.previous(direction, eight_way)
return (direction + (eight_way and -1 or -2)) % 8
end
--- Returns an orientation from a direction.
-- @tparam defines.direction direction
-- @treturn float
function Direction.to_orientation(direction)
return direction / 8
end
--- Returns a vector from a direction.
-- @tparam defines.direction direction
-- @tparam[opt = 1] number distance
-- @treturn Position
function Direction.to_vector(direction, distance)
distance = distance or 1
local x, y = 0, 0
if direction == Direction.north then
y = y - distance
elseif direction == Direction.northeast then
x, y = x + distance, y - distance
elseif direction == Direction.east then
x = x + distance
elseif direction == Direction.southeast then
x, y = x + distance, y + distance
elseif direction == Direction.south then
y = y + distance
elseif direction == Direction.southwest then
x, y = x - distance, y + distance
elseif direction == Direction.west then
x = x - distance
elseif direction == Direction.northwest then
x, y = x - distance, y - distance
end
return { x = x, y = y }
end
-- Deprecated
do
local Orientation = require('__stdlib__/stdlib/area/orientation')
Direction.opposite_direction = Direction.opposite
Direction.direction_to_orientation = Direction.to_orientation
function Direction.orientation_to_4way(orientation)
return Orientation.to_direction(orientation)
end
function Direction.orientation_to_8way(orientation)
return Orientation.to_direction(orientation, true)
end
function Direction.next_direction(direction, reverse, eight_way)
return (direction + (eight_way and ((reverse and -1) or 1) or ((reverse and -2) or 2))) % 8
end
end
return Direction

View File

@@ -0,0 +1,55 @@
--- Functions for working with orientations.
-- @module Area.Orientation
-- @usage local Orientation = require('__stdlib__/stdlib/area/orientation')
local Orientation = {
__class = 'Orientation',
__index = require('__stdlib__/stdlib/core'),
}
setmetatable(Orientation, Orientation)
--- north orientation
Orientation.north = defines.direction.north / 8
--- east orientation
Orientation.east = defines.direction.east / 8
--- west orientation
Orientation.west = defines.direction.west / 8
--- south orientation
Orientation.south = defines.direction.south / 8
--- northeast orientation
Orientation.northeast = defines.direction.northeast / 8
--- northwest orientation
Orientation.northwest = defines.direction.northwest / 8
--- southeast orientation
Orientation.southeast = defines.direction.southeast / 8
--- southwest orientation
Orientation.southwest = defines.direction.southwest / 8
local floor = math.floor
--- Returns a 4way or 8way direction from an orientation.
-- @tparam float orientation
-- @tparam[opt=false] boolean eight_way
-- @treturn defines.direction
function Orientation.to_direction(orientation, eight_way)
local ways = eight_way and 8 or 4
local mod = eight_way and 1 or 2
return floor(orientation * ways + 0.5) % ways * mod
end
--- Returns the opposite orientation.
-- @tparam float orientation
-- @treturn float the opposite orientation
function Orientation.opposite(orientation)
return (orientation + 0.5) % 1
end
--- Add two orientations together.
-- @tparam float orientation1
-- @tparam float orientation2
-- @treturn float the orientations added together
function Orientation.add(orientation1, orientation2)
return (orientation1 + orientation2) % 1
end
return Orientation

View File

@@ -0,0 +1,947 @@
--- Tools for working with `<x,y>` coordinates.
-- @module Area.Position
-- @usage local Position = require('__stdlib__/stdlib/area/position')
-- @see Area.Area
-- @see Concepts.Position
-- @see defines.direction
local Position = { __class = 'Position', __index = require('__stdlib__/stdlib/core') }
setmetatable(Position, Position)
local Direction = require('__stdlib__/stdlib/area/direction')
local Orientation = require('__stdlib__/stdlib/area/orientation')
local string = require('__stdlib__/stdlib/utils/string')
local math = require('__stdlib__/stdlib/utils/math')
local floor, abs, atan2, round_to, round = math.floor, math.abs, math.atan2, math.round_to, math.round
local cos, sin, ceil, sqrt, pi, random = math.cos, math.sin, math.ceil, math.sqrt, math.pi, math.random
local deg, acos, max, min, is_number = math.deg, math.acos, math.max, math.min, math.is_number
local split = string.split
local directions = defines.direction
local AREA_PATH = '__stdlib__/stdlib/area/area'
local EPSILON = 1.19e-07
local metatable
--- Constructor Methods
-- @section Constructors
Position.__call = function(_, ...)
local type = type((...))
if type == 'table' then
local t = (...)
if t.x and t.y then
return Position.load(...)
else
return Position.new(...)
end
elseif type == 'string' then
return Position.from_string(...)
else
return Position.construct(...)
end
end
local function new(x, y)
return setmetatable({ x = x, y = y }, metatable)
end
--- Returns a correctly formated position object.
-- @usage Position.new({0, 0}) -- returns {x = 0, y = 0}
-- @tparam Concepts.Position pos the position table or array to convert
-- @treturn Concepts.Position
function Position.new(pos)
return new(pos.x or pos[1] or 0, pos.y or pos[2] or 0)
end
--- Creates a table representing the position from x and y.
-- @tparam number x x-position
-- @tparam number y y-position
-- @treturn Concepts.Position
function Position.construct(...)
-- was self was passed as first argument?
local args = type((...)) == 'table' and { select(2, ...) } or { select(1, ...) }
return new(args[1] or 0, args[2] or args[1] or 0)
end
function Position.construct_xy(x, y)
return new(x, y)
end
--- Update a position in place without returning a new position.
-- @tparam Concepts.Position pos
-- @tparam number x
-- @tparam number y
-- @return Concepts.Position the passed position updated.
function Position.update(pos, x, y)
pos.x, pos.y = x, y
return pos
end
--- Load the metatable into the passed position without creating a new one.
-- Always assumes a valid position is passed
-- @tparam Concepts.Position pos the position to set the metatable onto
-- @treturn Concepts.Position the position with metatable attached
function Position.load(pos)
return setmetatable(pos, metatable)
end
--- Converts a position string to a position.
-- @tparam string pos_string the position to convert
-- @treturn Concepts.Position
function Position.from_string(pos_string)
return Position(load('return ' .. pos_string)())
end
--- Converts a string key position to a position.
-- @tparam string pos_string the position to convert
-- @treturn Concepts.Position
function Position.from_key(pos_string)
local tab = split(pos_string, ',', false, tonumber)
return new(tab[1], tab[2])
end
--- Gets the left top tile position of a chunk from the chunk position.
-- @tparam Concepts.Position pos
-- @treturn Concepts.Position
function Position.from_chunk_position(pos)
local x, y = (floor(pos.x) * 32), (floor(pos.y) * 32)
return new(x, y)
end
--- Convert position from pixels
-- @tparam Concepts.Position pos
-- @treturn Concepts.Position pos
function Position.from_pixels(pos)
local x = pos.x / 32
local y = pos.y / 32
return new(x, y)
end
--- Position Methods
-- @section Methods
--- Addition of two positions.
-- @tparam Concepts.Position pos1
-- @tparam Concepts.Position|number ... position or x, y values.
-- @treturn Concepts.Position pos1 with pos2 added
function Position.add(pos1, ...)
pos1 = Position(pos1)
local pos2 = Position(...)
return new(pos1.x + pos2.x, pos1.y + pos2.y)
end
--- Subtraction of two positions..
-- @tparam Concepts.Position pos1
-- @tparam Concepts.Position|number ... position or x, y values
-- @treturn Concepts.Position pos1 with pos2 subtracted
function Position.subtract(pos1, ...)
pos1 = Position(pos1)
local pos2 = Position(...)
return new(pos1.x - pos2.x, pos1.y - pos2.y)
end
--- Multiplication of two positions.
-- @tparam Concepts.Position pos1
-- @tparam Concepts.Position|number ... position or x, y values
-- @treturn Concepts.Position pos1 multiplied by pos2
function Position.multiply(pos1, ...)
pos1 = Position(pos1)
local pos2 = Position(...)
return new(pos1.x * pos2.x, pos1.y * pos2.y)
end
--- Division of two positions.
-- @tparam Concepts.Position pos1
-- @tparam Concepts.Position|number ... position or x, y values
-- @treturn Concepts.Position pos1 divided by pos2
function Position.divide(pos1, ...)
pos1 = Position(pos1)
local pos2 = Position(...)
return new(pos1.x / pos2.x, pos1.y / pos2.y)
end
--- Modulo of two positions.
-- @tparam Concepts.Position pos1
-- @tparam Concepts.Position|number ... position or x, y values
-- @treturn Concepts.Position pos1 modulo pos2
function Position.mod(pos1, ...)
pos1 = Position(pos1)
local pos2 = Position(...)
return new(pos1.x % pos2.x, pos1.y % pos2.y)
end
--- Return the closest position to the first position.
-- @tparam Concepts.Positions pos1 The position to find the closest too
-- @tparam array positions array of Concepts.Position
-- @treturn Concepts.Position
function Position.closest(pos1, positions)
local x, y = pos1.x, pos1.y
local closest = math.MAXINT32
for _, pos in pairs(positions) do
local distance = Position.distance(pos1, pos)
if distance < closest then
x, y = pos.x, pos.y
closest = distance
end
end
return new(x, y)
end
--- Return the farthest position from the first position.
-- @tparam Concepts.Positions pos1 The position to find the farthest from
-- @tparam array positions array of Concepts.Position
-- @treturn Concepts.Position
function Position.farthest(pos1, positions)
local x, y = pos1.x, pos1.y
local closest = 0
for _, pos in pairs(positions) do
local distance = Position.distance(pos1, pos)
if distance > closest then
x, y = pos.x, pos.y
closest = distance
end
end
return new(x, y)
end
--- The middle of two positions.
-- @tparam Concepts.Position pos1
-- @tparam Concepts.Position pos2
-- @treturn Concepts.Position pos1 the middle of two positions
function Position.between(pos1, pos2)
return new((pos1.x + pos2.x) / 2, (pos1.y + pos2.y) / 2)
end
--- The projection point of two positions.
-- @tparam Concepts.Position pos1
-- @tparam Concepts.Position pos2
-- @treturn Concepts.Position pos1 projected
function Position.projection(pos1, pos2)
local s = (pos1.x * pos2.x + pos1.y * pos2.y) / (pos2.x * pos2.x + pos2.y * pos2.y)
return new(s * pos2.x, s * pos2.y)
end
--- The reflection point or two positions.
-- @tparam Concepts.Position pos1
-- @tparam Concepts.Position pos2
-- @treturn Concepts.Position pos1 reflected
function Position.reflection(pos1, pos2)
local s = 2 * (pos1.x * pos2.x + pos1.y * pos2.y) / (pos2.x * pos2.x + pos2.y * pos2.y)
return new(s * pos2.x - pos1.x, s * pos2.y - pos1.y)
end
--- Stores the position for recall later, not deterministic.
-- Only the last position stored is saved.
-- @tparam Concepts.Position pos
function Position.store(pos)
rawset(getmetatable(pos), '_saved', pos)
return pos
end
--- Recalls the stored position.
-- @tparam Concepts.Position pos
-- @treturn Concepts.Position the stored position
function Position.recall(pos)
return rawget(getmetatable(pos), '_saved')
end
--- Normalizes a position by rounding it to 2 decimal places.
-- @tparam Concepts.Position pos
-- @treturn Concepts.Position a new normalized position
function Position.normalize(pos)
return new(round_to(pos.x, 2), round_to(pos.y, 2))
end
--- Abs x, y values
-- @tparam Concepts.Position pos
-- @treturn Concepts.Position
function Position.abs(pos)
return new(abs(pos.x), abs(pos.y))
end
--- Ceil x, y values.
-- @tparam Concepts.Position pos
-- @treturn Concepts.Position
function Position.ceil(pos)
return new(ceil(pos.x), ceil(pos.y))
end
--- Floor x, y values.
-- @tparam Concepts.Position pos
-- @treturn Concepts.Position
function Position.floor(pos)
return new(floor(pos.x), floor(pos.y))
end
local function pos_center(pos)
local x, y
local ceil_x = ceil(pos.x)
local ceil_y = ceil(pos.y)
x = pos.x >= 0 and floor(pos.x) + 0.5 or (ceil_x == pos.x and ceil_x + 0.5 or ceil_x - 0.5)
y = pos.y >= 0 and floor(pos.y) + 0.5 or (ceil_y == pos.y and ceil_y + 0.5 or ceil_y - 0.5)
return x, y
end
--- The center position of the tile where the given position resides.
-- @tparam Concepts.Position pos
-- @treturn Concepts.Position A new position at the center of the tile
function Position.center(pos)
return new(pos_center(pos))
end
--- Rounds a positions points to the closest integer.
-- @tparam Concepts.Position pos
-- @treturn Concepts.Position A new position rounded
function Position.round(pos)
return new(round(pos.x), round(pos.y))
end
--- Perpendicular position.
-- @tparam Concepts.Position pos
-- @treturn Concepts.Position pos
function Position.perpendicular(pos)
return new(-pos.y, pos.x)
end
--- Swap the x and y coordinates.
-- @tparam Concepts.Position pos
-- @treturn Concepts.Position A new position with x and y swapped
function Position.swap(pos)
return new(pos.y, pos.x)
end
--- Flip the signs of the position.
-- @tparam Concepts.Position pos
-- @return Concepts.Position A new position with flipped signs
function Position.flip(pos)
return new(-pos.x, -pos.y)
end
Position.unary = Position.flip
--- Flip the x sign.
-- @tparam Concepts.Position pos
-- @return Concepts.Position A new position with flipped sign on the x
function Position.flip_x(pos)
return new(-pos.x, pos.y)
end
--- Flip the y sign.
-- @tparam Concepts.Position pos
-- @return Concepts.Position A new position with flipped sign on the y
function Position.flip_y(pos)
return new(pos.x, -pos.y)
end
--- Lerp position of pos1 and pos2.
-- @tparam Concepts.Position pos1
-- @tparam Concepts.Position pos2
-- @tparam float alpha 0-1 how close to get to position 2
-- @treturn Concepts.Position the lerped position
function Position.lerp(pos1, pos2, alpha)
local x = pos1.x + (pos2.x - pos1.x) * alpha
local y = pos1.y + (pos2.y - pos1.y) * alpha
return new(x, y)
end
--- Trim the position to a length.
-- @tparam Concepts.Position pos
-- @tparam number max_len
function Position.trim(pos, max_len)
local s = max_len * max_len / (pos.x * pos.x + pos.y * pos.y)
s = (s > 1 and 1) or sqrt(s)
return new(pos.x * s, pos.y * s)
end
--- Returns the position along line between source and target, at the distance from target.
-- @tparam Concepts.Position pos1 where the line starts and extends from.
-- @tparam Concepts.Position pos2 where the line ends and is offset back from.
-- @tparam number distance_from_pos2 backwards from pos1 for the new position.
-- @treturn Concepts.Position a point along line between source and target, at requested offset back from target.
function Position.offset_along_line(pos1, pos2, distance_from_pos2)
distance_from_pos2 = distance_from_pos2 or 0
local angle = Position.atan2(pos1, pos2)
local veclength = Position.distance(pos1, pos2) - distance_from_pos2
-- From source_position, project the point along the vector at angle, and veclength
local x = pos1.x + round_to(sin(angle) * veclength, 2)
local y = pos1.y + round_to(cos(angle) * veclength, 2)
return new(x, y)
end
--- Translates a position in the given direction.
-- @tparam Concepts.Position pos the position to translate
-- @tparam defines.direction direction the direction of translation
-- @tparam number distance distance of the translation
-- @treturn Concepts.Position a new translated position
function Position.translate(pos, direction, distance)
direction = direction or 0
distance = distance or 1
return Position.add(pos, Direction.to_vector(direction, distance))
end
--- Return a random offset of a position.
-- @tparam Concepts.Position pos the position to randomize
-- @tparam[opt=0] number minimum the minimum amount to offset
-- @tparam[opt=1] number maximum the maximum amount to offset
-- @tparam[opt=false] boolean random_tile randomize the location on the tile
-- @treturn Concepts.Position a new random offset position
function Position.random(pos, minimum, maximum, random_tile)
local rand_x = random(minimum or 0, maximum or 1)
local rand_y = random(minimum or 0, maximum or 1)
local x = pos.x + (random() >= .5 and -rand_x or rand_x) + (random_tile and random() or 0)
local y = pos.y + (random() >= .5 and -rand_y or rand_y) + (random_tile and random() or 0)
return new(x, y)
end
local function get_array(...)
local array = select(2, ...)
if array then
table.insert(array, (...))
else
array = (...)
end
return array
end
--- Return the average position of the passed positions.
-- @tparam array positions array of Concepts.Position
-- @treturn Concepts.Position a new position
function Position.average(...)
local positions = get_array(...)
local avg = new(0, 0)
for _, pos in ipairs(positions) do Position.add(avg, pos) end
return Position.divide(avg, #positions)
end
--- Return the minimum position of the passed positions.
-- @tparam array positions array of Concepts.Position
-- @treturn Concepts.Position a new position
function Position.min(...)
local positions = get_array(...)
local x, y
local len = math.MAXINT32
for _, pos in pairs(positions) do
local cur_len = Position.len(pos)
if cur_len < len then
len = cur_len
x, y = pos.x, pos.y
end
end
return new(x, y)
end
--- Return the maximum position of the passed positions.
-- @tparam array positions array of Concepts.Position
-- @treturn Concepts.Position a new position
function Position.max(...)
local positions = get_array(...)
local x, y
local len = -math.MAXINT32
for _, pos in pairs(positions) do
local cur_len = Position.len(pos)
if cur_len > len then
len = cur_len
x, y = pos.x, pos.y
end
end
return new(x, y)
end
--- Return a position created from the smallest x, y values in the passed positions.
-- @tparam array positions array of Concepts.Position
-- @treturn Concepts.Position a new position
function Position.min_xy(...)
local positions = get_array(...)
local x, y = positions[1].x, positions[1].y
for _, pos in pairs(positions) do
x = min(x, pos.x)
y = min(y, pos.y)
end
return new(x, y)
end
--- Return a position created from the largest x, y values in the passed positions.
-- @tparam array positions array of Concepts.Position
-- @treturn Concepts.Position a new position
function Position.max_xy(...)
local positions = get_array(...)
local x, y = positions[1].x, positions[1].y
for _, pos in pairs(positions) do
x = max(x, pos.x)
y = max(y, pos.y)
end
return new(x, y)
end
--- The intersection of 4 positions.
-- @treturn Concepts.Position a new position
function Position.intersection(pos1_start, pos1_end, pos2_start, pos2_end)
local d = (pos1_start.x - pos1_end.x) * (pos2_start.y - pos2_end.y) - (pos1_start.y - pos1_end.y) *
(pos2_start.x - pos2_end.x)
local a = pos1_start.x * pos1_end.y - pos1_start.y * pos1_end.x
local b = pos2_start.x * pos2_end.y - pos2_start.y * pos2_end.x
local x = (a * (pos2_start.x - pos2_end.x) - (pos1_start.x - pos1_end.x) * b) / d
local y = (a * (pos2_start.y - pos2_end.y) - (pos1_start.y - pos1_end.y) * b) / d
return is_number(x) and is_number(y) and new(x, y) or pos1_start
end
--- Position Mutate Methods
-- @section Mutate Methods
--- Normalizes a position by rounding it to 2 decimal places.
-- @tparam Concepts.Position pos
-- @treturn Concepts.Position the normalized position mutated
function Position.normalized(pos)
pos.x, pos.y = round_to(pos.x, 2), round_to(pos.y, 2)
return pos
end
--- Abs x, y values
-- @tparam Concepts.Position pos
-- @treturn Concepts.Position the absolute position mutated
function Position.absed(pos)
pos.x, pos.y = abs(pos.x), abs(pos.y)
return pos
end
--- Ceil x, y values in place.
-- @tparam Concepts.Position pos
-- @treturn Concepts.Position the ceiled position mutated
function Position.ceiled(pos)
pos.x, pos.y = ceil(pos.x), ceil(pos.y)
return pos
end
--- Floor x, y values.
-- @tparam Concepts.Position pos
-- @treturn Concepts.Position the floored position mutated
function Position.floored(pos)
pos.x, pos.y = floor(pos.x), floor(pos.y)
return pos
end
--- The center position of the tile where the given position resides.
-- @tparam Concepts.Position pos
-- @treturn Concepts.Position the centered position mutated
function Position.centered(pos)
pos.x, pos.y = pos_center(pos)
return pos
end
--- Rounds a positions points to the closest integer.
-- @tparam Concepts.Position pos
-- @treturn Concepts.Position the rounded position mutated
function Position.rounded(pos)
pos.x, pos.y = round(pos.x), round(pos.y)
return pos
end
--- Swap the x and y coordinates.
-- @tparam Concepts.Position pos
-- @treturn Concepts.Position the swapped position mutated
function Position.swapped(pos)
pos.x, pos.y = pos.y, pos.x
return pos
end
--- Flip the signs of the position.
-- @tparam Concepts.Position pos
-- @return Concepts.Position the flipped position mutated
function Position.flipped(pos)
pos.x, pos.y = -pos.x, -pos.y
return pos
end
--- Position Conversion Methods
-- @section Position Conversion Methods
-- Test Comment
--- Convert to pixels from position
-- @tparam Concepts.Position pos
-- @treturn Concepts.Position pos
function Position.to_pixels(pos)
local x = pos.x * 32
local y = pos.y * 32
return new(x, y)
end
--- Gets the chunk position of a chunk where the specified position resides.
-- @tparam Concepts.Position pos a position residing somewhere in a chunk
-- @treturn Concepts.ChunkPosition a new chunk position
-- @usage local chunk_x = Position.chunk_position(pos).x
function Position.to_chunk_position(pos)
local x, y = floor(pos.x / 32), floor(pos.y / 32)
return new(x, y)
end
--- Area Conversion Methods
-- @section Area Conversion Methods
-- Hackish function, Factorio lua doesn't allow require inside functions because...
local function load_area(area)
local Area = package.loaded[AREA_PATH]
if not Area then
local log = log or function(_msg_)
end
log('WARNING: Area for Position not found in package.loaded')
end
return Area and Area.load(area) or area
end
--- Expands a position to a square area.
-- @tparam Concepts.Position pos the position to expand into an area
-- @tparam number radius half of the side length of the area
-- @treturn Concepts.BoundingBox the area
function Position.expand_to_area(pos, radius)
radius = radius or 1
local left_top = { x = pos.x - radius, y = pos.y - radius }
local right_bottom = { x = pos.x + radius, y = pos.y + radius }
return load_area { left_top = left_top, right_bottom = right_bottom }
end
--- Expands a position into an area by setting pos to left_top.
-- @tparam Concepts.Position pos
-- @tparam number width
-- @tparam number height
-- @treturn Concepts.BoundingBox
function Position.to_area(pos, width, height)
width = width or 0
height = height or width
local left_top = { x = pos.x, y = pos.y }
local right_bottom = { x = pos.x + width, y = pos.y + height }
return load_area { left_top = left_top, right_bottom = right_bottom }
end
--- Converts a tile position to the @{Concepts.BoundingBox|area} of the tile it is in.
-- @tparam LuaTile.position pos the tile position
-- @treturn Concepts.BoundingBox the area of the tile
function Position.to_tile_area(pos)
local x, y = floor(pos.x), floor(pos.y)
local left_top = { x = x, y = y }
local right_bottom = { x = x + 1, y = y + 1 }
return load_area { left_top = left_top, right_bottom = right_bottom }
end
--- Get the chunk area the specified position is in.
-- @tparam Concepts.Position pos
-- @treturn Concepts.BoundingBox
function Position.to_chunk_area(pos)
local left_top = { x = floor(pos.x / 32) * 32, y = floor(pos.y / 32) * 32 }
local right_bottom = { x = left_top.x + 32, y = left_top.y + 32 }
return load_area { left_top = left_top, right_bottom = right_bottom }
end
--- Get the chunk area for the specified chunk position.
-- @tparam Concepts.ChunkPosition pos
-- @treturn Concepts.BoundingBox The chunks positions area
function Position.chunk_position_to_chunk_area(pos)
local left_top = { x = pos.x * 32, y = pos.y * 32 }
local right_bottom = { left_top.x + 32, left_top.y + 32 }
return load_area { left_top = left_top, right_bottom = right_bottom }
end
--- Position Functions
-- @section Functions
--- Gets the squared length of a position
-- @tparam Concepts.Position pos
-- @treturn number
function Position.len_squared(pos)
return pos.x * pos.x + pos.y * pos.y
end
--- Gets the length of a position
-- @tparam Concepts.Position pos
-- @treturn number
function Position.len(pos)
return (pos.x * pos.x + pos.y * pos.y) ^ 0.5
end
--- Converts a position to a string.
-- @tparam Concepts.Position pos the position to convert
-- @treturn string string representation of the position
function Position.to_string(pos)
return '{x = ' .. pos.x .. ', y = ' .. pos.y .. '}'
end
--- Converts a position to an x, y string.
-- @tparam Concepts.Position pos the position to convert
-- @treturn string
function Position.to_string_xy(pos)
return pos.x .. ', ' .. pos.y
end
--- Converts a position to a string suitable for using as a table index.
-- @tparam Concepts.Position pos the position to convert
-- @treturn string
function Position.to_key(pos)
return pos.x .. ',' .. pos.y
end
--- Unpack a position into a tuple.
-- @tparam Concepts.Position pos the position to unpack
-- @treturn tuple x, y
function Position.unpack(pos)
return pos.x, pos.y
end
--- Packs a position into an array.
-- @tparam Concepts.Position pos the position to pack
-- @treturn array
function Position.pack(pos)
return { pos.x, pos.y }
end
--- Is this position {0, 0}.
-- @tparam Concepts.Position pos
-- @treturn boolean
function Position.is_zero(pos)
return pos.x == 0 and pos.y == 0
end
--- Is a position inside of an area.
-- @tparam Concepts.Position pos The pos to check
-- @tparam Concepts.BoundingBox area The area to check.
-- @treturn boolean Is the position inside of the area.
function Position.inside(pos, area)
local lt = area.left_top
local rb = area.right_bottom
return pos.x >= lt.x and pos.y >= lt.y and pos.x <= rb.x and pos.y <= rb.y
end
--- Is this a simple position. {num, num}
-- @tparam Concepts.Position pos
-- @treturn boolean
function Position.is_simple_position(pos)
return type(pos) == 'table' and type(pos[1]) == 'number' and type(pos[2]) == 'number'
end
--- Is this a complex position. {x = number, y = number}
-- @tparam Concepts.Position pos
-- @treturn boolean
function Position.is_complex_position(pos)
return type(pos) == 'table' and type(pos.x) == 'number' and type(pos.y) == 'number'
end
--- Does the position have the class attached
-- @tparam Concepts.Position pos
-- @treturn boolean
function Position.is_Position(pos)
return getmetatable(pos) == metatable
end
--- Is this any position
-- @tparam Concepts.Position pos
-- @treturn boolean
function Position.is_position(pos)
return Position.is_Position(pos) or Position.is_complex_position(pos) or Position.is_simple_position(pos)
end
--- Return the atan2 of 2 positions.
-- @tparam Concepts.Position pos1
-- @tparam Concepts.Position pos2
-- @treturn number
function Position.atan2(pos1, pos2)
return atan2(pos2.x - pos1.x, pos2.y - pos1.y)
end
--- The angle between two positions
-- @tparam Concepts.Position pos1
-- @tparam Concepts.Position pos2
-- @treturn number
function Position.angle(pos1, pos2)
local dist = Position.distance(pos1, pos2)
if dist ~= 0 then
return deg(acos((pos1.x * pos2.x + pos1.y * pos2.y) / dist))
else
return 0
end
end
--- Return the cross product of two positions.
-- @tparam Concepts.Position pos1
-- @tparam Concepts.Position pos2
-- @treturn number
function Position.cross(pos1, pos2)
return pos1.x * pos2.y - pos1.y * pos2.x
end
-- Return the dot product of two positions.
-- @tparam Concepts.Position pos1
-- @tparam Concepts.Position pos2
-- @treturn number
function Position.dot(pos1, pos2)
return pos1.x * pos2.x + pos1.y * pos2.y
end
--- Tests whether or not the two given positions are equal.
-- @tparam Concepts.Position pos1
-- @tparam Concepts.Position pos2
-- @treturn boolean true if positions are equal
function Position.equals(pos1, pos2)
if not (pos1 and pos2) then return false end
return abs(pos1.x - pos2.x) < EPSILON and abs(pos1.y - pos2.y) < EPSILON
end
--- Is pos1 less than pos2.
-- @tparam Concepts.Position pos1
-- @tparam Concepts.Position pos2
-- @treturn boolean
function Position.less_than(pos1, pos2)
return Position.len(pos1) < Position.len(pos2)
end
--- Is pos1 less than or equal to pos2.
-- @tparam Concepts.Position pos1
-- @tparam Concepts.Position pos2
-- @treturn boolean
function Position.less_than_eq(pos1, pos2)
return Position.len(pos1) <= Position.len(pos2)
end
--- Calculates the Euclidean distance squared between two positions, useful when sqrt is not needed.
-- @tparam Concepts.Position pos1
-- @tparam[opt] Concepts.Position pos2
-- @treturn number the square of the euclidean distance
function Position.distance_squared(pos1, pos2)
local ax_bx = pos1.x - pos2.x
local ay_by = pos1.y - pos2.y
return ax_bx * ax_bx + ay_by * ay_by
end
--- Calculates the Euclidean distance between two positions.
-- @tparam Concepts.Position pos1
-- @tparam[opt={x=0, y=0}] Concepts.Position pos2
-- @treturn number the euclidean distance
function Position.distance(pos1, pos2)
local ax_bx = pos1.x - pos2.x
local ay_by = pos1.y - pos2.y
return (ax_bx * ax_bx + ay_by * ay_by) ^ 0.5
end
--- Calculates the manhatten distance between two positions.
-- @tparam Concepts.Position pos1
-- @tparam[opt] Concepts.Position pos2 the second position
-- @treturn number the manhatten distance
-- @see https://en.wikipedia.org/wiki/Taxicab_geometry Taxicab geometry (manhatten distance)
function Position.manhattan_distance(pos1, pos2)
return abs(pos2.x - pos1.x) + abs(pos2.y - pos1.y)
end
--- Returms the direction to a position using simple delta comparisons.
-- @tparam Concepts.Position pos1
-- @tparam Concepts.Position pos2
-- @treturn defines.direction
function Position.direction_to(pos1, pos2)
local dx = pos1.x - pos2.x
local dy = pos1.y - pos2.y
if dx ~= 0 then
if dy == 0 then
return dx > 0 and directions.west or directions.east
else
local adx, ady = abs(dx), abs(dy)
if adx > ady then
return dx > 0 and directions.north or directions.south
else
return dy > 0 and directions.west or directions.east
end
end
else
return dy > 0 and directions.north or directions.south
end
end
--- Returns the direction to a position.
-- @tparam Concepts.Position pos1
-- @tparam Concepts.Position pos2
-- @tparam boolean eight_way return the eight way direction
-- @treturn defines.direction
function Position.complex_direction_to(pos1, pos2, eight_way)
return Orientation.to_direction(Position.orientation_to(pos1, pos2), eight_way)
end
function Position.orientation_to(pos1, pos2)
return (1 - (Position.atan2(pos1, pos2) / pi)) / 2
end
--- Increment a position each time it is called.
-- This can be used to increment or even decrement a position quickly.
-- <p>Do not store function closures in the global object; use them in the current tick.
-- @usage
-- local next_pos = Position.increment({0,0})
-- for i = 1, 5 do next_pos(0,1) -- returns {x = 0, y = 1} {x = 0, y = 2} {x = 0, y = 3} {x = 0, y = 4} {x = 0, y = 5}
-- @usage
-- local next_pos = Position.increment({0, 0}, 1)
-- next_pos() -- returns {1, 0}
-- next_pos(0, 5) -- returns {1, 5}
-- next_pos(nil, 5) -- returns {2, 10}
-- @usage
-- local next_pos = Position.increment({0, 0}, 0, 1)
-- surface.create_entity{name = 'flying-text', text = 'text', position = next_pos()}
-- surface.create_entity{name = 'flying-text', text = 'text', position = next_pos()} -- creates two flying text entities 1 tile apart
-- @tparam Concepts.Position pos the position to start with
-- @tparam[opt=0] number inc_x optional increment x by this amount
-- @tparam[opt=0] number inc_y optional increment y by this amount
-- @tparam[opt=false] boolean increment_initial Whether the first use should be incremented
-- @treturn function @{increment_closure} a function closure that returns a new incremented position
function Position.increment(pos, inc_x, inc_y, increment_initial)
local x, y = pos.x, pos.y
inc_x, inc_y = inc_x or 0, inc_y or 0
--- A closure which the @{increment} function returns.
-- @function increment_closure
-- > Do not call this directly and do not store this in the global object.
-- @see increment
-- @tparam[opt=0] number new_inc_x
-- @tparam[opt=0] number new_inc_y
-- @treturn Concepts.Position the incremented position
return function(new_inc_x, new_inc_y)
if increment_initial then
x = x + (new_inc_x or inc_x)
y = y + (new_inc_y or inc_y)
else
x = x
y = y
increment_initial = true
end
return new(x, y)
end
end
--- Metamethods
-- @section Metamethods
--- Position tables are returned with these metamethods attached.
-- Methods that return a position will return a NEW position without modifying the passed positions.
-- @table Metamethods
metatable = {
__class = 'position',
__index = Position, -- If key is not found, see if there is one availble in the Position module.
__add = Position.add, -- Adds two position together. Returns a new position.
__sub = Position.subtract, -- Subtracts one position from another. Returns a new position.
__mul = Position.multiply, -- Multiply 2 positions. Returns a new position.
__div = Position.divide, -- Divide 2 positions. Returns a new position.
__mod = Position.mod, -- Modulo of 2 positions. Returns a new position.
__unm = Position.flip, -- Unary Minus of a position. Returns a new position.
__len = Position.len, -- Length of a single position.
__eq = Position.equals, -- Are two positions at the same spot.
__lt = Position.less_than, -- Is position1 less than position2.
__le = Position.less_than_eq, -- Is position1 less than or equal to position2.
__tostring = Position.to_string, -- Returns a string representation of the position
__concat = _ENV.concat, -- calls tostring on both sides of concact.
__call = Position.new, -- copy the position.
__debugline = [[<Position>{[}x={x},y={y}{]}]]
}
return Position

View File

@@ -0,0 +1,155 @@
--- For working with surfaces.
-- Surfaces are the "domain" of the world.
-- @module Area.Surface
-- @usage local Surface = require('__stdlib__/stdlib/area/surface')
-- @see LuaSurface
local Surface = {
__class = 'Surface',
__index = require('__stdlib__/stdlib/core')
}
setmetatable(Surface, Surface)
local Is = require('__stdlib__/stdlib/utils/is')
local Area = require('__stdlib__/stdlib/area/area')
--- Flexible and safe lookup function for surfaces.
-- <ul>
-- <li>May be given a single surface name or an array of surface names in @{string}.
-- <li>May be given a single surface object or an array of surface objects in @{LuaSurface}.
-- <li>May also be given a @{nil}.
-- <li>Returns an array of surface objects of all valid and existing surfaces.
-- <li>Returns an empty array if no surfaces are given or if they are not found.
-- </ul>
-- @tparam ?|nil|string|{string,...}|LuaSurface|{LuaSurface,...} surface the surfaces to look up
-- @treturn {nil|LuaSurface,...} an array of all valid surfaces or nil otherwise
function Surface.lookup(surface)
if not surface then
return {}
end
if type(surface) == 'string' or type(surface) == 'number' then
local lookup = game.surfaces[surface]
if lookup then
return { lookup }
end
return {}
end
if type(surface) == 'table' and surface['__self'] then
return Surface.lookup(surface.name)
end
local results = {}
for _, surface_item in pairs(surface) do
if type(surface_item) == 'string' then
if game.surfaces[surface_item] then
table.insert(results, game.surfaces[surface_item])
end
elseif type(surface_item) == 'table' and surface_item['__self'] then
table.insert(results, surface_item)
end
end
return results
end
--- Given a @{search_criteria|search criteria}, find all entities that match the criteria.
-- <ul>
-- <li>If ***search\_criteria.name*** is not supplied, search for entities with any name.
-- <li>If ***search\_criteria.type*** is not supplied, search for entities of any type.
-- <li>If ***search\_criteria.force*** is not supplied, search for entities owned by any force.
-- <li>If ***search\_criteria.surface*** is not supplied, search for entities on all surfaces.
-- <li>If ***search\_criteria.area*** is not supplied, search the entire specified surface.
-- </ul>
-- @usage
-- surface.find_all_entities({ type = 'unit', surface = 'nauvis', area = {{-1000,20},{-153,2214}})
-- -- returns a list containing all unit entities on the nauvis surface in the given area
-- @param search_criteria (<span class="types">@{search_criteria}</span>) a table used to search for entities
-- @treturn {nil|LuaEntity,...} an array of all entities that matched the criteria **OR** *nil* if there were no matches
function Surface.find_all_entities(search_criteria)
Is.Assert.Table(search_criteria, 'missing search_criteria argument')
if search_criteria.name == nil and search_criteria.type == nil and search_criteria.force == nil and search_criteria.area == nil then
error('Missing search criteria field: name or type or force or area of entity', 2)
end
local surface_list = Surface.lookup(search_criteria.surface)
if search_criteria.surface == nil then
surface_list = game.surfaces
end
local results = {}
for _, surface in pairs(surface_list) do
local entities =
surface.find_entities_filtered
{
area = search_criteria.area,
name = search_criteria.name,
type = search_criteria.type,
force = search_criteria.force
}
for _, entity in pairs(entities) do
table.insert(results, entity)
end
end
return results
end
---
-- This table should be passed into @{find_all_entities} function to find entities that match the criteria.
-- @tfield[opt] string name internal name of an entity &mdash; (example: "locomotive")
-- @tfield[opt] string type type of an entity &mdash; (example: "unit")
-- @tfield[opt] string|LuaForce force the force of an entity &mdash; (examples: "neutral", "enemy")
-- @tfield[opt] ?|nil|string|{string,...}|LuaSurface|{LuaSurface,...} surface the surface to search &mdash; (example: "nauvis")
-- @tfield[opt] Concepts.BoundingBox area the area to search
-- @table search_criteria
--- Gets the area which covers the entirety of a given surface.
-- This function is useful if you wish to compare the total number of chunks against the number of chunks within the entire area of a given surface.
-- @tparam LuaSurface surface the surface for which to get the area
-- @treturn Concepts.BoundingBox the area of a given surface
function Surface.get_surface_bounds(surface)
Is.Assert(surface, 'missing surface value')
local x1, y1, x2, y2 = 0, 0, 0, 0
for chunk in surface.get_chunks() do
if chunk.x < x1 then
x1 = chunk.x
elseif chunk.x > x2 then
x2 = chunk.x
end
if chunk.y < y1 then
y1 = chunk.y
elseif chunk.y > y2 then
y2 = chunk.y
end
end
return Area.construct(x1 * 32, y1 * 32, x2 * 32, y2 * 32)
end
--- Sets the daytime transition thresholds on a given surface
-- @tparam LuaSurface surface the surface for which to set the thresholds
-- @tparam number morning daytime to begin transition from dark to light
-- @tparam number dawn daytime to finish transition from dark to light
-- @tparam number dusk daytime to begin transition from light to dark
-- @tparam number evening daytime to finish transition from light to dark
-- @treturn boolean true if the thresholds were set, false if there was an error
-- @return[opt] the raised error, if any
function Surface.set_daytime_thresholds(surface, morning, dawn, dusk, evening)
Is.Assert.Valid(surface, 'missing surface value')
Is.Assert(Is.Float(morning) and Is.Float(dawn) and Is.float(dusk) and Is.Float(evening), 'paramaters must be floats')
return pcall(
function()
surface.dusk = 0
surface.evening = .0000000001
surface.morning = .0000000002
surface.dawn = dawn
surface.morning = morning
surface.evening = evening
surface.dusk = dusk
end
)
end
return Surface

View File

@@ -0,0 +1,90 @@
--- Tools for working with tiles.
-- A tile represents a 1 unit<sup>2</sup> on a surface in Factorio.
-- @module Area.Tile
-- @usage local Tile = require('__stdlib__/stdlib/area/tile')
-- @see LuaTile
local Tile = {
__class = 'Tile',
__index = require('__stdlib__/stdlib/core')
}
setmetatable(Tile, Tile)
local Is = require('__stdlib__/stdlib/utils/is')
local Game = require('__stdlib__/stdlib/game')
local Position = require('__stdlib__/stdlib/area/position')
Tile.__call = Position.__call
--- Get the @{LuaTile.position|tile position} of a tile where the given position resides.
-- @function Tile.from_position
-- @see Area.Position.floor
Tile.from_position = Position.floor
--- Converts a tile position to the @{Concepts.BoundingBox|area} of the tile it is in.
-- @function Tile.to_area
-- @see Area.Position.to_tile_area
Tile.to_area = Position.to_tile_area
--- Creates an array of tile positions for all adjacent tiles (N, E, S, W) **OR** (N, NE, E, SE, S, SW, W, NW) if diagonal is set to true.
-- @tparam LuaSurface surface the surface to examine for adjacent tiles
-- @tparam LuaTile.position position the tile position of the origin tile
-- @tparam[opt=false] boolean diagonal whether to include diagonal tiles
-- @tparam[opt] string tile_name whether to restrict adjacent tiles to a particular tile name (example: "water-tile")
-- @treturn {LuaTile.position,...} an array of tile positions of the tiles that are adjacent to the origin tile
function Tile.adjacent(surface, position, diagonal, tile_name)
Is.Assert(surface, 'missing surface argument')
Is.Assert(position, 'missing position argument')
local offsets = { { 0, 1 }, { 1, 0 }, { 0, -1 }, { -1, 0 } }
if diagonal then
offsets = { { 0, 1 }, { 1, 1 }, { 1, 0 }, { -1, 1 }, { -1, 0 }, { -1, -1 }, { 0, -1 }, { 1, -1 } }
end
local adjacent_tiles = {}
for _, offset in pairs(offsets) do
local adj_pos = Position.add(position, offset)
if tile_name then
local tile = surface.get_tile(adj_pos.x, adj_pos.y)
if tile and tile.name == tile_name then
table.insert(adjacent_tiles, adj_pos)
end
else
table.insert(adjacent_tiles, adj_pos)
end
end
return adjacent_tiles
end
--- Gets the user data that is associated with a tile.
-- The user data is stored in the global object and it persists between loads.
-- @tparam LuaSurface surface the surface on which the user data is looked up
-- @tparam LuaTile.position tile_pos the tile position on which the user data is looked up
-- @tparam[opt] Mixed default_value the user data to set for the tile and returned if it did not have user data
-- @treturn ?|nil|Mixed the user data **OR** *nil* if it does not exist for the tile and no default_value was set
function Tile.get_data(surface, tile_pos, default_value)
surface = Game.get_surface(surface)
assert(surface, 'invalid surface')
local key = Position.to_key(Position.floor(tile_pos))
return Game.get_or_set_data('_tile_data', surface.index, key, false, default_value)
end
Tile.get = Tile.get_data
--- Associates the user data to a tile.
-- The user data will be stored in the global object and it will persist between loads.
-- @tparam LuaSurface surface the surface on which the user data will reside
-- @tparam LuaTile.position tile_pos the tile position of a tile that will be associated with the user data
-- @tparam ?|nil|Mixed value the user data to set **OR** *nil* to erase the existing user data for the tile
-- @treturn ?|nil|Mixed the previous user data associated with the tile **OR** *nil* if the tile had no previous user data
function Tile.set_data(surface, tile_pos, value)
surface = Game.get_surface(surface)
assert(surface, 'invalid surface')
local key = Position.to_key(Position.floor(tile_pos))
return Game.get_or_set_data('_tile_data', surface.index, key, true, value)
end
Tile.set = Tile.set_data
return Tile

View File

@@ -0,0 +1 @@
return {}

View File

@@ -0,0 +1,123 @@
--- The Core module loads some helper functions and globals useful in all stages
-- of a mods life cycle. All modules have an __index method into core.
-- @module Core
-- @usage local Core = require('__stdlib__/stdlib/core')
-- require global helper functions.
require('__stdlib__/stdlib/utils/globals')
local Core = {
_VERSION = '1.0.0',
_DESCRIPTION = 'Factorio Lua Standard Library Project',
_URL = 'https://github.com/Afforess/Factorio-Stdlib',
_LICENSE = [[
MIT LICENSE
Copyright (c) 2016, Afforess
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
]],
__class = 'Core',
__call = function(t, ...)
return t:__call(...)
end,
__config = require('__stdlib__/stdlib/config')
}
Core.Unique_Array = require('__stdlib__/stdlib/utils/classes/unique_array')
Core.String_Array = Core.Unique_Array
--- Prints and logs the msg
-- @tparam string msg
-- @treturn boolean true if the message was printed to someone
function Core.log_and_print(msg)
if game and #game.connected_players > 0 then
log(script.mod_name .. ':' .. msg)
game.print(msg)
return true
else
log(msg)
end
end
if script then
---Simple valid check, only available in control stage.
---@deprecated
function Core.VALID_FILTER(v)
return v and v.valid
end
function Core.get_file_path(append)
return script.mod_name .. '/' .. append
end
end
local function no_meta(item, path)
if path[#path] == _ENV.inspect.METATABLE then
return { item.__class }
end
return item
end
--- Inspect the class
function Core.inspect(self)
return _ENV.inspect(self, { process = no_meta })
end
--- Help function available on everything.
function Core.help(self)
local help_string = ''
local tab = self
local pat = '^%_%_%_'
local function sort(a, b)
if b:find(pat) then
return false
end
return a:find(pat) or a < b
end
local function build_string()
local keys = {}
for key in pairs(tab) do
if type(key) ~= 'number' and not key:find('^%_%w') then
if key == '__class' then
key = '___' .. tab.__class
end
keys[#keys + 1] = key
end
end
table.sort(keys, sort)
help_string = help_string .. table.concat(keys, ', ') .. '\n\n'
end
while (type(tab) == 'table') do
build_string()
local old_meta = tab
tab = getmetatable(tab)
if tab then
if tab ~= old_meta then
build_string()
end
if type(tab.__index) == 'function' then
tab = tab.__parent
elseif type(tab.__index == 'table') then
tab = tab.__index
end
end
end
return help_string
end
return Core

View File

@@ -0,0 +1,41 @@
--- Category
-- @classmod Data.Category
local Category = {
__class = 'Category',
__index = require('__stdlib__/stdlib/data/data'),
__call = require('__stdlib__/stdlib/data/data').__call
}
setmetatable(Category, Category)
Category.category_types = {
['ammo-category'] = true,
['equipment-category'] = true,
['fuel-category'] = true,
['recipe-category'] = true,
['module-category'] = true,
['rail-category'] = true,
['resource-category'] = true
}
function Category:create()
return self
end
function Category:add(_)
return self
end
function Category:remove(_)
return self
end
function Category:replace(a, b)
if self:valid('category') then
self:remove(a)
self:add(b)
end
return self
end
return Category

View File

@@ -0,0 +1,522 @@
--- Data
-- @classmod Data
require('__stdlib__/stdlib/core') -- Calling core up here to setup any required global stuffs
if _G.remote and _G.script then
error('Data Modules can only be required in the data stage', 2)
end
local Table = require('__stdlib__/stdlib/utils/table')
local groups = require('__stdlib__/stdlib/data/modules/groups')
local Data = {
__class = 'Data',
__index = require('__stdlib__/stdlib/core'),
Sprites = require('__stdlib__/stdlib/data/modules/sprites'),
Pipes = require('__stdlib__/stdlib/data/modules/pipes'),
Util = require('__stdlib__/stdlib/data/modules/util'),
_default_options = {
['silent'] = false, -- Don't log if not present
['fail'] = false, -- Error instead of logging
['verbose'] = false, -- Extra logging info
['extend'] = true, -- Extend the data
['skip_string_validity'] = false, -- Skip checking for valid data
['items_and_fluids'] = true -- consider fluids valid for Item checks
}
}
setmetatable(Data, Data)
local inspect = _ENV.inspect
local rawtostring = _ENV.rawtostring
--))
--(( Local Functions ))--
-- This is the tracing function.
local function log_trace(self, object, object_type)
local msg = (self.__class and self.__class or '') .. (self.name and '/' .. self.name or '') .. ' '
msg = msg .. (object_type and (object_type .. '/') or '') .. tostring(object) .. ' does not exist.'
local trace = _ENV.data_traceback()
log(msg .. trace)
end
--)) END Local Functions ((--
--(( METHODS ))--
--- Is this a valid object
-- @tparam[opt] string type if present is the object this type
-- @treturn self
function Data:is_valid(type)
if type then
return rawget(self, 'valid') == type or false
else
return rawget(self, 'valid') and true or false
end
end
function Data:is_class(class)
if class then
return self.__class == class or false
else
return self.__class and true or false
end
end
function Data:print(...)
local arr = {}
for _, key in pairs { ... } do
arr[#arr + 1] = inspect(self[key])
end
print(Table.unpack(arr))
return self
end
function Data:log(tbl)
local reduce_spam = function(item, path)
-- if item == self.class then
-- return {item.__class, self.__class}
-- end
if item == self._object_mt then
return { self.__class, tostring(self) }
end
if path[#path] == 'parent' then
return { tostring(item), item.__class }
end
if path[#path] == 'class' then
return { self.__class, item.__class }
end
if path[#path] == inspect.METATABLE then
return { self.__class or item.__class, item.__class }
end
return item
end
log(inspect(tbl and tbl or self, { process = reduce_spam }))
return self
end
function Data:serpent()
log(serpent.block(self, { name = self.name, metatostring = false, nocode = true, comment = false }))
return self
end
function Data:error(msg)
error(msg or 'Forced Error')
return self
end
--- Changes the validity of the object.
-- @tparam boolean bool
-- @treturn self
function Data:continue(bool)
rawset(self, 'valid', (bool and rawget(self, '_raw') and self.type) or false)
return self
end
--- Changes the validity of the object if the passed function is true.
-- @tparam function func the function to test, self is passed as the first paramater
-- @treturn self
function Data:continue_if(fun, ...)
rawset(self, 'valid', (fun(self, ...) and rawget(self, '_raw') and self.type) or false)
return self
end
--- Extend object into the data table
-- @tparam[opt] boolean force Extend even if it is already extended
-- @treturn self
function Data:extend(force)
if self.valid and (self.options.extend or force) then
if not self.extended or force then
local t = data.raw[self.type]
if t == nil then
t = {}
data.raw[self.type] = t
end
t[self.name] = self._raw
self.extended = true
end
end
if force then
log('NOTICE: Force extend ' .. self.type .. '/' .. self.name)
elseif not self.options.extend and not self.extended then
log('NOTICE: Did not extend ' .. self.type .. '/' .. self.name)
end
if self.overwrite then
log('NOTICE: Overwriting ' .. self.type .. '/' .. self.name)
end
return self
end
--- Copies a recipe to a new recipe.
-- @tparam string new_name The new name for the recipe.
-- @tparam string result
-- @tparam[opt] table opts
-- @treturn self
function Data:copy(new_name, result, opts)
assert(type(new_name) == 'string', 'new_name must be a string')
if self:is_valid() then
result = result or new_name
local copy = Table.deep_copy(rawget(self, '_raw'))
copy.name = new_name
-- For Entities
-- Need to also check mining results!!!!!!
if copy.minable and copy.minable.result then
copy.minable.result = result
end
-- For items
if copy.place_result then
copy.place_result = result
end
-- rail planners
if copy.placeable_by and copy.placeable_by.item then
copy.placeable_by.item = result
end
-- For recipes, should also to check results!!
if copy.type == 'recipe' then
if copy.normal and copy.normal.result then
copy.normal.result = new_name
copy.expensive.result = new_name
else
if copy.result then
copy.result = new_name
end
end
end
return self(copy, nil, opts or self.options)
else
error('Cannot Copy, invalid prototype', 4)
end
end
--(( Flags ))--
function Data:Flags(create)
if create then
self.flags = Data.Unique_Array(self.flags)
end
return self.flags or Data.Unique_Array()
end
function Data:add_flag(flag)
self:Flags(true):add(flag)
return self
end
function Data:remove_flag(flag)
self:Flags(true):remove(flag)
return self
end
function Data:has_flag(flag)
return self:Flags():all(flag)
end
function Data:any_flag(flag)
return self:Flags():all(flag)
end
--)) Flags ((--
--- Run a function if the object is valid.
-- The object and any additional paramaters are passed to the function.
-- @tparam function func then function to run.
-- @treturn self
function Data:run_function(fun, ...)
if self:is_valid() then
fun(self, ...)
end
return self
end
Data.execute = Data.run_function
--- Run a function on a valid object and return its results.
-- @tparam function func the function to run. self is passed as the first paramter
-- @treturn boolean if the object was valid
-- @treturn the results from the passed function
function Data:get_function_results(fun, ...)
if self:is_valid() then
return true, fun(self, ...)
end
end
--- Set the unique array class to the field if the field is present
-- @tparam table tab
-- @treturn self
function Data:set_unique_array(tab)
if self:is_valid() and tab then
self.Unique_Array(tab)
end
return self
end
--- Add or change a field.
-- @tparam string field the field to change.
-- @tparam mixed value the value to set on the field.
-- @treturn self
function Data:set_field(field, value)
self[field] = value
return self
end
Data.set = Data.set_field
--- Iterate a dictionary table and set fields on the object. Existing fields are overwritten.
-- @tparam table tab dictionary table of fields to set.
-- @treturn self
function Data:set_fields(tab)
if self:is_valid() then
for field, value in pairs(tab) do
self[field] = value
end
end
return self
end
--- Get a field.
-- @tparam string field
-- @tparam mixed default_value return this if the field doesn't exist
-- @treturn nil|mixed the value of the field
function Data:get_field(field, default_value)
if self:is_valid() then
local has = self[field]
if has ~= nil then
return has
else
return default_value
end
end
end
--- Iterate an array of fields and return the values as paramaters
-- @tparam array arr
-- @tparam boolean as_dictionary Return the results as a dictionary table instead of parameters
-- @treturn mixed the parameters
-- @usage local icon, name = Data('stone-furnace', 'furnace'):get_fields({icon, name})
function Data:get_fields(arr, as_dictionary)
if self:is_valid() then
local values = {}
for _, name in pairs(arr) do
values[as_dictionary and name or #values + 1] = self[name]
end
return as_dictionary and values or Table.unpack(values)
end
end
--- Remove an indiviual field from the the object
-- @tparam string field The field to remove
-- @treturn self
function Data:remove_field(field)
if self:is_valid() then
self[field] = nil
end
return self
end
--- Iterate a string array and set to nil.
-- @tparam table arr string array of fields to remove.
-- @treturn self
function Data:remove_fields(arr)
if self:is_valid() then
for _, field in pairs(arr) do
self[field] = nil
end
end
return self
end
--- Change the item-subgroup and/or order.
-- @tparam[opt=nil] string subgroup, The subgroup to change to if valid.
-- @tparam[opt=nil] string order The order string to use
-- note if subgroup is non nil and subgroub is not valid order wil not be changed.
-- @treturn self
function Data:subgroup_order(subgroup, order)
if self:is_valid() then
if subgroup then
if data.raw['item-subgroup'][subgroup] then
self.subgroup = subgroup
else
order = false
end
end
if order and #order > 0 then
self.order = order
end
end
return self
end
--- Replace an icon
-- @tparam string icon
-- @tparam int size
function Data:replace_icon(icon, size)
if self:is_valid() then
if type(icon) == 'table' then
self.icons = icon
self.icon = nil
else
self.icon = icon
end
self.icon_size = size or self.icon_size
end
if not self.icon_size then
error('Icon present but icon size not detected')
end
return self
end
--- Get the icons
-- @tparam[opt=false] boolean copy return a copy of the icons table
-- @treturn table icons
function Data:get_icons(copy)
if self:is_valid() then
return copy and Table.deep_copy(self.icons) or self.icons
end
end
function Data:get_icon()
if self:is_valid() then
return self.icon
end
end
function Data:make_icons(...)
if self:is_valid() then
if not self.icons then
if self.icon then
self.icons = { { icon = self.icon, icon_size = self.icon_size } }
self.icon = nil
else
self.icons = {}
end
end
for _, icon in pairs { ... } do
self.icons[#self.icons + 1] = Table.deep_copy(icon)
end
end
return self
end
function Data:set_icon_at(index, values)
if self:is_valid() then
if self.icons then
for k, v in pairs(values or {}) do
self.icons[index].k = v
end
end
end
return self
end
--- Get the objects name.
-- @treturn string the objects name
function Data:tostring()
return self.valid and (self.name and self.type) and (self.type .. '/' .. self.name) or rawtostring(self)
end
function Data:pairs(source, opts)
local index, val
if not source and self.type then
source = data.raw[self.type]
else
local type = type(source)
source = type == 'string' and data.raw[source] or (assert(type == 'table', 'Source missing') and source)
end
---@cast source -false
local function _next()
index, val = next(source, index)
if index then
return index, self(val, nil, opts)
end
end
return _next, index, val
end
--- Returns a valid thing object reference. This is the main getter.
-- @tparam string|table object The thing to use, if string the thing must be in data.raw[type], tables are not verified
-- @tparam[opt] string object_type the raw type. Required if object is a string
-- @tparam[opt] table opts options to pass
-- @treturn Object
function Data:get(object, object_type, opts)
--assert(type(object) == 'string' or type(object) == 'table', 'object string or table is required')
-- Create our middle man container object
local new = {
class = self.class or self,
_raw = nil,
_products = nil,
_parent = nil,
valid = false,
extended = false,
overwrite = false,
options = Table.merge(Table.deep_copy(Data._default_options), opts or self.options or {})
}
if type(object) == 'table' then
assert(object.type and object.name, 'name and type are required')
new._raw = object
new.valid = object.type
--Is a data-raw that we are overwriting
local existing = data.raw[object.type] and data.raw[object.type][object.name]
new.extended = existing == object
new.overwrite = not new.extended and existing and true or false
elseif type(object) == 'string' then
--Get type from object_type, or fluid or item_and_fluid_types
local types = (object_type and { object_type }) or (self.__class == 'Item' and groups.item_and_fluid)
if types then
for _, type in pairs(types) do
new._raw = data.raw[type] and data.raw[type][object]
if new._raw then
new.valid = new._raw.type
new.extended = true
break
end
end
else
error('object_type is missing for ' .. (self.__class or 'Unknown') .. '/' .. (object or ''), 3)
end
end
setmetatable(new, self._object_mt)
if new.valid then
rawset(new, '_parent', new)
self.Unique_Array.set(new.flags)
self.Unique_Array.set(new.crafting_categories)
self.Unique_Array.set(new.mining_categories)
self.Unique_Array.set(new.inputs)
elseif not new.options.silent then
log_trace(new, object, object_type)
end
return new:extend()
end
Data.__call = Data.get
--)) END Methods ((--
-- This is the table set on new objects
Data._object_mt = {
--__class = "Data",
-- index from _raw if that is not available then retrieve from the class
__index = function(t, k)
return rawget(t, '_raw') and t._raw[k] or t.class[k]
end,
-- Only allow setting on valid _raw tables.
__newindex = function(t, k, v)
if rawget(t, 'valid') and rawget(t, '_raw') then
t._raw[k] = v
end
end,
-- Call the getter on itself
__call = function(t, ...)
return t:__call(...)
end,
-- use Core.tostring
__tostring = Data.tostring
}
return Data

View File

@@ -0,0 +1,73 @@
--- Developer
-- @script Developer
local Data = require('__stdlib__/stdlib/data/data')
local Developer = {
__index = Data
}
setmetatable(Developer, Developer)
local function make_no_controls()
local controls = {}
for name in pairs(data.raw['autoplace-control']) do
if name == 'grass' then
controls[name] = { size = 'very-high', frequency = 'very-high', richness = 'very-low' }
else
controls[name] = { size = 'none', frequency = 'very-low', richness = 'very-low' }
end
end
return controls
end
--- Make entities for easier mod testing.
-- @tparam string name The name of your mod
-- @usage
-- --data.lua
-- local Developer = require('__stdlib__/stdlib/data/develper/developer')
-- Developer.make_test_entities()
function Developer.make_test_entities()
log('Making developer debug entities')
if not data.raw['electric-energy-interface']['debug-energy-interface'] then
Data('electric-energy-interface', 'electric-energy-interface'):copy('debug-energy-interface'):set_fields {
flags = { 'placeable-off-grid' },
localised_name = 'Debug source',
icon = data.raw['item']['electric-energy-interface'].icon,
collision_mask = {},
selection_box = { { 0.0, -0.5 }, { 0.5, 0.5 } },
picture = Developer.Sprites.empty_picture()
}:remove_fields { 'minable', 'collision_box', 'vehicle_impact_sound', 'working_sound' }
end
if not data.raw['electric-pole']['debug-substation'] then
Data('substation', 'electric-pole'):copy('debug-substation'):set_fields {
localised_name = 'Debug power substation',
flags = { 'placeable-off-grid' },
icon = data.raw['item']['substation'].icon,
selection_box = { { -0.5, -0.5 }, { 0.0, 0.5 } },
collision_mask = {},
pictures = Developer.Sprites.empty_pictures(),
maximum_wire_distance = 64,
supply_area_distance = 64,
connection_points = Developer.Sprites.empty_connection_points(1)
}:remove_fields { 'minable', 'collision_box', 'vehicle_impact_sound', 'working_sound' }
end
local gen = data.raw['map-gen-presets']
gen['default']['debug'] = {
type = 'map-gen-presets',
name = 'debug',
localised_name = 'Debug',
localised_description = 'Default settings for a debug world',
order = 'z',
basic_settings = {
terrain_segmentation = 'very-low',
water = 'none',
autoplace_controls = make_no_controls(),
height = 128,
width = 128
},
}
end
return Developer

View File

@@ -0,0 +1,43 @@
--- Entity class
-- @classmod Data.Entity
local Data = require('__stdlib__/stdlib/data/data')
local Entity = {
__class = 'Entity',
__index = Data,
__call = Data.__call
}
setmetatable(Entity, Entity)
function Entity:get_minable_item()
local Item = require('__stdlib__/stdlib/data/item')
if self:is_valid() then
local m = self.minable
return Item(m and (m.result or (m.results and (m.results[1] or m.results.name))), nil, self.options)
end
return Item()
end
function Entity:is_player_placeable()
if self:is_valid() then
return self:Flags():any('player-creation', 'placeable-player')
end
return false
end
function Entity:change_lab_inputs(name, add)
if self:is_valid('lab') then
Entity.Unique_Array.set(self.inputs)
if add then
self.inputs:add(name)
else
self.inputs:remove(name)
end
else
log('Entity is not a lab.' .. _ENV.data_traceback())
end
return self
end
return Entity

View File

@@ -0,0 +1,16 @@
--- Fluid
-- @classmod Data.Fluid
local Data = require('__stdlib__/stdlib/data/data')
local Fluid = {
__class = 'Fluid',
__index = Data,
}
function Fluid:__call(fluid)
return self:get(fluid, 'fluid')
end
setmetatable(Fluid, Fluid)
return Fluid

View File

@@ -0,0 +1,44 @@
--- Item
-- @classmod Data.Item
local Data = require('__stdlib__/stdlib/data/data')
local Table = require('__stdlib__/stdlib/utils/table')
local Item = {
__class = 'Item',
__index = Data,
__call = Data.__call
}
setmetatable(Item, Item)
local function make_table(params)
if not params then
return Table.keys(data.raw.lab)
else
return type(params) == 'table' and params or { params }
end
end
local function change_inputs(name, lab_names, add)
lab_names = make_table(lab_names)
local Entity = require('__stdlib__/stdlib/data/entity')
for _, lab_name in pairs(lab_names) do
Entity(lab_name, 'lab'):change_lab_inputs(name, add)
end
end
function Item:add_to_labs(lab_names)
if self:is_valid() then
change_inputs(self.name, lab_names, true)
end
return self
end
function Item:remove_from_labs(lab_names)
if self:is_valid() then
change_inputs(self.name, lab_names, false)
end
return self
end
return Item

View File

@@ -0,0 +1,15 @@
// {type:class}
// {direction:topDown}
[Data{bg:red}]->[Recipe]
[Recipe]->[Raw]
[Ingredients]
[Results]
[Products]^[Ingredients]
[Products]^[Results]
[Recipe]->[Ingredients]
[Recipe]->[Results]
[Parent]<->[note: Get the damn parent]
[Ingredients|parent;child]
//[Ingredients]->[Parent]
[Results]->[Parent]
[Parent]->[Recipe]

View File

@@ -0,0 +1,199 @@
local table = require('__stdlib__/stdlib/utils/table')
local groups = {}
groups.category = {
'ammo-category',
'equipment-category',
'fuel-category',
'item-group',
'item-subgroup',
'module-category',
'rail-category',
'recipe-category',
'resource-category',
}
groups.equipment = {
'active-defense-equipment',
'battery-equipment',
'belt-immunity-equipment',
'energy-shield-equipment',
'generator-equipment',
'movement-bonus-equipment',
'night-vision-equipment',
'roboport-equipment',
'solar-panel-equipment',
}
groups.other = {
'ambient-sound',
'autoplace-control',
'combat-robot-count',
'damage-type',
'equipment-grid',
'font',
'gui-style',
'map-gen-presets',
'map-settings',
'market',
'noise-expression',
'noise-layer',
'optimized-decorative',
'recipe',
'technology',
'tile',
'train-path-achievement',
'trivial-smoke',
'tutorial',
}
groups.utility = {
'utility-constants',
'utility-sounds',
'utility-sprites'
}
groups.signal = {
'virtual-signal'
}
groups.achievement = {
'achievement',
'build-entity-achievement',
'construct-with-robots-achievement',
'deconstruct-with-robots-achievement',
'deliver-by-robots-achievement',
'dont-build-entity-achievement',
'dont-craft-manually-achievement',
'dont-use-entity-in-energy-production-achievement',
'finish-the-game-achievement',
'group-attack-achievement',
'kill-achievement',
'player-damaged-achievement',
'produce-achievement',
'produce-per-hour-achievement',
'research-achievement',
}
groups.item = {
'item',
'ammo',
'armor',
'blueprint-book',
'blueprint',
'capsule',
'deconstruction-item',
'gun',
'item-with-entity-data',
'item-with-inventory',
'item-with-label',
'item-with-tags',
'module',
'rail-planner',
'repair-tool',
'selection-tool',
'tool',
}
groups.entity = {
'accumulator',
'ammo-turret',
'arithmetic-combinator',
'arrow',
'artillery-flare',
'artillery-projectile',
'artillery-turret',
'artillery-wagon',
'assembling-machine',
'beacon',
'beam',
'boiler',
'car',
'cargo-wagon',
'character-corpse',
'cliff',
'combat-robot',
'constant-combinator',
'construction-robot',
'container',
'corpse',
'curved-rail',
'decider-combinator',
'deconstructible-tile-proxy',
'decorative',
'electric-energy-interface',
'electric-pole',
'electric-turret',
'entity-ghost',
'explosion',
'fire',
'fish',
'flame-thrower-explosion',
'fluid-turret',
'fluid-wagon',
'flying-text',
'furnace',
'gate',
'generator',
'god-controller',
'heat-pipe',
'infinity-container',
'inserter',
'item-entity',
'item-request-proxy',
'lab',
'lamp',
'land-mine',
'leaf-particle',
'loader',
'locomotive',
'logistic-container',
'logistic-robot',
'mining-drill',
'offshore-pump',
'particle',
'particle-source',
'pipe',
'pipe-to-ground',
'player',
'player-port',
'power-switch',
'programmable-speaker',
'projectile',
'pump',
'radar',
'rail-chain-signal',
'rail-remnants',
'rail-signal',
'reactor',
'resource',
'roboport',
'rocket-silo',
'rocket-silo-rocket',
'rocket-silo-rocket-shadow',
'simple-entity',
'simple-entity-with-force',
'simple-entity-with-owner',
'smoke',
'smoke-with-trigger',
'solar-panel',
'splitter',
'sticker',
'storage-tank',
'straight-rail',
'stream',
'tile-ghost',
'train-stop',
'transport-belt',
'tree',
'turret',
'underground-belt',
'unit',
'unit-spawner',
'wall'
}
groups.fluid = {
'fluid'
}
groups.item_and_fluid = table.array_combine(groups.item, groups.fluid)
return groups

View File

@@ -0,0 +1,93 @@
render layers
"tile-transition", "resource", "decorative", "remnants", "floor", "transport-belt-endings", "corpse",
"floor-mechanics", "item", "lower-object", "object", "higher-object-above", "higher-object-under",
"wires", "lower-radius-visualization", "radius-visualization", "entity-info-icon", "explosion",
"projectile", "smoke", "air-object", "air-entity-info-con", "light-effect", "selection-box", "arrow", "cursor"
collision masks
"ground-tile", "water-tile", "resource-layer", "floor-layer", "item-layer",
"object-layer", "player-layer", "ghost-layer", "doodad-layer", "not-colliding-with-itself"
-------------------------------------------------------------------------------
Data(name, type) Data:Get()
Data:valid(type)
Data:copy()
Data:Flags()
Data:extend()
Data:continue()
Data:continue_if(func)
Data:subgroup_order()
Data:run_function(func) Data:execute()
Data:get_function_results(func)
Recipe:set_field()
Recipe:set_fields()
Recipe:remove_field()
Recipe:remove_fields()
------------------------------------------------------------------------------
Recipe()
Recipe:Ingredients() -- wrapper to Products(type)
Recipe:Results() -- wrapper to Products(type)
Recipe:Products(recipe, type, difficulty)
:get(recipe, type, difficulty)
:add()
:remove()
:replace()
:convert()
:clear()
:recipe() -- return recipe object
:make_difficult()
:make_simple()
-- affects top level
:change_category()
.crafting_machine_tint
-- Wrappers to recipe difficulty data (set_field)
-- Maybe override set_field/s()
.ingredients, -- table
.results, -- table
.result, -- string
.result_count, -- number
-- string fields
.main_product,
-- number fields
.energy_required,
.requester_paste_multiplier,
.overload_multiplier,
.emissions_multiplier,
-- bool fields
.enabled,
.hidden,
.hide_from_stats,
.allow_decomposition,
.allow_as_intermediate,
.allow_intermediates,
.always_show_made_in,
.show_amount_in_title,
.always_show_products,
:set_hidden()
:set_enabled()
-- Wrappers to Products()
:clear_ingredients()
:replace_ingredients()
:replace_ingredient()
:add_ingredient()
:remove_ingredient()
:clear_results()
:replace_results()
:replace_result()
:add_result()
:remove_result()
-- Wrappers to Technology()
:add_unlock()
:remove_unlock()

View File

@@ -0,0 +1,208 @@
--- Pipes
-- @module Data.Pipes
local Pipes = {}
local Sprites = require('__stdlib__/stdlib/data/modules/sprites')
--Define pipe connection pipe pictures, not all entities use these. This function needs some work though.
function Pipes.pictures(pictures, shift_north, shift_south, shift_west, shift_east, replacements)
local new_pictures = {
north = shift_north and
{
filename = '__base__/graphics/entity/' .. pictures .. '/' .. pictures .. '-pipe-N.png',
priority = 'extra-high',
width = 35,
height = 18,
shift = shift_north
} or
Sprites.empty_picture(),
south = shift_south and
{
filename = '__base__/graphics/entity/' .. pictures .. '/' .. pictures .. '-pipe-S.png',
priority = 'extra-high',
width = 44,
height = 31,
shift = shift_south
} or
Sprites.empty_picture(),
west = shift_west and
{
filename = '__base__/graphics/entity/' .. pictures .. '/' .. pictures .. '-pipe-W.png',
priority = 'extra-high',
width = 19,
height = 37,
shift = shift_west
} or
Sprites.empty_picture(),
east = shift_east and
{
filename = '__base__/graphics/entity/' .. pictures .. '/' .. pictures .. '-pipe-E.png',
priority = 'extra-high',
width = 20,
height = 38,
shift = shift_east
} or
Sprites.empty_picture()
}
for direction, image in pairs(replacements or {}) do
if not (new_pictures[direction].filename == '__core__/graphics/empty.png') then
new_pictures[direction].filename = image.filename
new_pictures[direction].width = image.width
new_pictures[direction].height = image.height
new_pictures[direction].priority = image.priority or new_pictures[direction].priority
end
end
return new_pictures
end
--return pipe covers for true directions.
function Pipes.covers(n, s, w, e)
if (n == nil and s == nil and w == nil and e == nil) then
n, s, e, w = true, true, true, true
end
n =
n and
{
layers = {
{
filename = '__base__/graphics/entity/pipe-covers/pipe-cover-north.png',
priority = 'extra-high',
width = 64,
height = 64,
hr_version = {
filename = '__base__/graphics/entity/pipe-covers/hr-pipe-cover-north.png',
priority = 'extra-high',
width = 128,
height = 128,
scale = 0.5
}
},
{
filename = '__base__/graphics/entity/pipe-covers/pipe-cover-north-shadow.png',
priority = 'extra-high',
width = 64,
height = 64,
draw_as_shadow = true,
hr_version = {
filename = '__base__/graphics/entity/pipe-covers/hr-pipe-cover-north-shadow.png',
priority = 'extra-high',
width = 128,
height = 128,
scale = 0.5,
draw_as_shadow = true
}
}
}
} or
Sprites.empty_picture()
e =
e and
{
layers = {
{
filename = '__base__/graphics/entity/pipe-covers/pipe-cover-east.png',
priority = 'extra-high',
width = 64,
height = 64,
hr_version = {
filename = '__base__/graphics/entity/pipe-covers/hr-pipe-cover-east.png',
priority = 'extra-high',
width = 128,
height = 128,
scale = 0.5
}
},
{
filename = '__base__/graphics/entity/pipe-covers/pipe-cover-east-shadow.png',
priority = 'extra-high',
width = 64,
height = 64,
draw_as_shadow = true,
hr_version = {
filename = '__base__/graphics/entity/pipe-covers/hr-pipe-cover-east-shadow.png',
priority = 'extra-high',
width = 128,
height = 128,
scale = 0.5,
draw_as_shadow = true
}
}
}
} or
Sprites.empty_picture()
s =
s and
{
layers = {
{
filename = '__base__/graphics/entity/pipe-covers/pipe-cover-south.png',
priority = 'extra-high',
width = 64,
height = 64,
hr_version = {
filename = '__base__/graphics/entity/pipe-covers/hr-pipe-cover-south.png',
priority = 'extra-high',
width = 128,
height = 128,
scale = 0.5
}
},
{
filename = '__base__/graphics/entity/pipe-covers/pipe-cover-south-shadow.png',
priority = 'extra-high',
width = 64,
height = 64,
draw_as_shadow = true,
hr_version = {
filename = '__base__/graphics/entity/pipe-covers/hr-pipe-cover-south-shadow.png',
priority = 'extra-high',
width = 128,
height = 128,
scale = 0.5,
draw_as_shadow = true
}
}
}
} or
Sprites.empty_picture()
w =
w and
{
layers = {
{
filename = '__base__/graphics/entity/pipe-covers/pipe-cover-west.png',
priority = 'extra-high',
width = 64,
height = 64,
hr_version = {
filename = '__base__/graphics/entity/pipe-covers/hr-pipe-cover-west.png',
priority = 'extra-high',
width = 128,
height = 128,
scale = 0.5
}
},
{
filename = '__base__/graphics/entity/pipe-covers/pipe-cover-west-shadow.png',
priority = 'extra-high',
width = 64,
height = 64,
draw_as_shadow = true,
hr_version = {
filename = '__base__/graphics/entity/pipe-covers/hr-pipe-cover-west-shadow.png',
priority = 'extra-high',
width = 128,
height = 128,
scale = 0.5,
draw_as_shadow = true
}
}
}
} or
Sprites.empty_picture()
return { north = n, south = s, east = e, west = w }
end
return Pipes

View File

@@ -0,0 +1,56 @@
type, name
localised_name[opt]
localised_description[opt]
subgroup, order (needed when no main product)
recipe
category
icon/icons[opt] (or has main_product)
crafting_machine_tint = {
primary, secondary, tertiary
}
normal/expensive = {
ingredients
results, result, result_count[opt=1] (result ignored if results present) at least 1 result
main_product
energy_required > 0.001
emissions_multiplier
requester_paste_multiplier
overload_multiplier
enabled <boolean>
hidden <boolean>
hide_from_stats <boolean>
allow_decomposition <boolean>
allow_as_intermediate <boolean>
allow_intermediates <boolean>
always_show_made_in <boolean>
show_amount_in_title <boolean>
always_show_products <boolean>
}
Ingredients
{"name", amount} -- Assumes a type of "item"
{
type :: string: "item" or "fluid".
name :: string: Prototype name of the required item or fluid.
amount :: uint: Amount of the item or fluid.
minimum_temperature :: uint (optional): The minimum fluid temperature required. Has no effect if type is '"item"'.
maximum_temperature :: uint (optional): The maximum fluid temperature allowed. Has no effect if type is '"item"'.
}
Results
{
type :: string: "item" or "fluid".
name :: string: Prototype name of the result.
amount :: float (optional): If not specified, amount_min, amount_max and probability must all be specified.
temperature :: uint (optional): The fluid temperature of this product. Has no effect if type is '"item"'.
amount_min :: uint (optional):
amount_max :: uint (optional):
probability :: double (optional): A value in range [0, 1].
}
{
"name", amount -- assumes a type of item
}
result -- assumes type of item
result_count[opt = 1]

View File

@@ -0,0 +1,83 @@
--- Sprites
-- @module Data.Sprites
local Sprites = {}
function Sprites.extract_monolith(filename, x, y, w, h)
return {
type = 'monolith',
top_monolith_border = 0,
right_monolith_border = 0,
bottom_monolith_border = 0,
left_monolith_border = 0,
monolith_image = {
filename = filename,
priority = 'extra-high-no-scale',
width = w,
height = h,
x = x,
y = y
}
}
end
--- Quick to use empty picture.
-- @treturn table an empty pictures table
function Sprites.empty_picture()
return {
filename = '__core__/graphics/empty.png',
priority = 'extra-high',
width = 1,
height = 1
}
end
Sprites.empty_sprite = Sprites.empty_picture
--- Quick to use empty pictures.
-- @treturn table an empty pictures table
function Sprites.empty_pictures()
local empty = Sprites.empty_picture()
return {
filename = empty.filename,
width = empty.width,
height = empty.height,
line_length = 1,
frame_count = 1,
shift = { 0, 0 },
animation_speed = 1,
direction_count = 1
}
end
Sprites.empty_animation = Sprites.empty_pictures
--- Quick to use empty animation.
-- @treturn table an empty animations table
function Sprites.empty_animations()
return {
Sprites.empty_pictures()
}
end
--- Quick to use empty connections table.
-- @tparam int count how many connection points are needed
-- @treturn table an empty pictures table
function Sprites.empty_connection_points(count)
local points = {}
for i = 1, count or 1, 1 do
points[i] = {
shadow = {
copper = { 0, 0 },
green = { 0, 0 },
red = { 0, 0 }
},
wire = {
copper = { 0, 0 },
green = { 0, 0 },
red = { 0, 0 }
}
}
end
return points
end
return Sprites

View File

@@ -0,0 +1,73 @@
--- Data Utilities
-- @module Data.Util
local Util = {
__class = 'Util',
__index = require('__stdlib__/stdlib/core')
}
setmetatable(Util, Util)
local Is = require('__stdlib__/stdlib/utils/is')
local table = require('__stdlib__/stdlib/utils/table')
function Util.extend(proto_array)
Is.Assert.Table(proto_array, 'Missing table or array to extend')
data:extend(#proto_array > 0 and proto_array or { proto_array })
end
function Util.disable_control(control)
if data.raw['custom-input'] and data.raw['custom-input'][control] then
data.raw['custom-input'][control].enabled = false
end
end
function Util.extend_style(style)
data.raw['gui-style'].default[style.name] = style
end
function Util.extend_style_by_name(name, style)
data.raw['gui-style'].default[name] = style
end
--- Quickly duplicate an existing prototype into a new one.
-- @tparam string data_type The type of the object to duplicate
-- @tparam string orig_name The name of the object to duplicate
-- @tparam string new_name The new name to use.
-- @tparam[opt] string|boolean mining_result If true set mining_result to new_name, if truthy set mining_result to value
function Util.duplicate(data_type, orig_name, new_name, mining_result)
mining_result = type(mining_result) == 'boolean' and new_name or mining_result
if data.raw[data_type] and data.raw[data_type][orig_name] then
local proto = table.deep_copy(data.raw[data_type][orig_name])
proto.name = new_name
if mining_result then
if proto.minable and proto.minable.result then
proto.minable.result = mining_result
end
end
if proto.place_result then
proto.place_result = new_name
end
if proto.result then
proto.result = new_name
end
return (proto)
else
error('Unknown Prototype ' .. data_type .. '/' .. orig_name)
end
end
-- load the data portion of stdlib into globals, by default it loads everything into an ALLCAPS name.
-- Alternatively you can pass a dictionary of `[global names] -> [require path]`.
-- @tparam[opt] table files
-- @treturn Data
-- @usage
-- require('__stdlib__/stdlib/data/data).util.create_data_globals()
function Util.create_data_globals(files)
_ENV.STDLIB.create_stdlib_data_globals(files)
end
return Util

View File

@@ -0,0 +1,427 @@
--- Recipe class
-- @classmod Data.Recipe
local Data = require('__stdlib__/stdlib/data/data')
local Table = require('__stdlib__/stdlib/utils/table')
local Recipe = {
__class = 'Recipe',
__index = Data,
}
function Recipe:__call(recipe)
local new = self:get(recipe, 'recipe')
-- rawset(new, 'Ingredients', {})
-- rawset(new, 'Results', {})
return new
end
setmetatable(Recipe, Recipe)
-- Returns a formated ingredient or prodcut table
local function format(ingredient, result_count)
local Item = require('__stdlib__/stdlib/data/item')
local object
if type(ingredient) == 'table' then
if ingredient.valid and ingredient:is_valid() then
return ingredient
elseif ingredient.name then
if Item(ingredient.name, ingredient.type):is_valid() then
object = Table.deepcopy(ingredient)
if not object.amount and not (object.amount_min and object.amount_max and object.probability) then
error('Result table requires amount or probabilities')
end
end
elseif #ingredient > 0 then
-- Can only be item types not fluid
local item = Item(ingredient[1])
if item:is_valid() and item.type ~= 'fluid' then
object = {
type = 'item',
name = ingredient[1],
amount = ingredient[2] or 1
}
end
end
elseif type(ingredient) == 'string' then
-- Our shortcut so we need to check it
local item = Item(ingredient)
if item:is_valid() then
object = {
type = item.type == 'fluid' and 'fluid' or 'item',
name = ingredient,
amount = result_count or 1
}
end
end
return object
end
-- Format items for difficulties
-- If expensive is a boolean then return a copy of normal for expensive
local function get_difficulties(normal, expensive)
return format(normal), format((expensive == true and Table.deepcopy(normal)) or expensive)
end
--- Remove an ingredient from an ingredients table.
-- @tparam table ingredients
-- @tparam string name Name of the ingredient to remove
local function remove_ingredient(ingredients, name)
name = name.name
for i, ingredient in pairs(ingredients or {}) do
if ingredient[1] == name or ingredient.name == name then
ingredients[i] = nil
return true
end
end
end
--- Replace an ingredient.
-- @tparam table ingredients Ingredients table
-- @tparam string find ingredient to replace
-- @tparam concepts.ingredient replace
-- @tparam boolean replace_name_only Don't replace amounts
local function replace_ingredient(ingredients, find, replace, replace_name_only)
for i, ingredient in pairs(ingredients or {}) do
if ingredient[1] == find or ingredient.name == find then
if replace_name_only then
local amount = ingredient[2] or ingredient.amount
replace.amount = amount
end
ingredients[i] = replace
return true
end
end
end
--- Add an ingredient to a recipe.
-- @tparam string|Concepts.ingredient normal
-- @tparam[opt] string|Concepts.ingredient|boolean expensive
-- @treturn Recipe
function Recipe:add_ingredient(normal, expensive)
if self:is_valid() then
normal, expensive = get_difficulties(normal, expensive)
if self.normal then
if normal then
self.normal.ingredients = self.normal.ingredients or {}
self.normal.ingredients[#self.normal.ingredients + 1] = normal
end
if expensive then
self.expensive.ingredients = self.expensive.ingredients or {}
self.expensive.ingredients[#self.expensive.ingredients + 1] = expensive
end
elseif normal then
self.ingredients = self.ingredients or {}
self.ingredients[#self.ingredients + 1] = normal
end
end
return self
end
Recipe.add_ing = Recipe.add_ingredient
--- Remove one ingredient completely.
-- @tparam string normal
-- @tparam string|boolean expensive expensive recipe to remove, or if true remove normal recipe from both
-- @treturn Recipe
function Recipe:remove_ingredient(normal, expensive)
if self:is_valid() then
normal, expensive = get_difficulties(normal, expensive)
if self.normal then
if normal then
remove_ingredient(self.normal.ingredients, normal)
end
if expensive then
remove_ingredient(self.expensive.ingredients, expensive)
end
elseif normal then
remove_ingredient(self.ingredients, normal)
end
end
return self
end
Recipe.rem_ing = Recipe.remove_ingredient
--- Replace one ingredient with another.
-- @tparam string replace
-- @tparam string|ingredient normal
-- @tparam[opt] string|ingredient|boolean expensive
function Recipe:replace_ingredient(replace, normal, expensive)
assert(replace, 'Missing recipe to replace')
if self:is_valid() then
local n_string = type(normal) == 'string'
local e_string = type(expensive == true and normal or expensive) == 'string'
normal, expensive = get_difficulties(normal, expensive)
if self.normal then
if normal then
replace_ingredient(self.normal.ingredients, replace, normal, n_string)
end
if expensive then
replace_ingredient(self.expensive.ingredients, replace, expensive, e_string)
end
elseif normal then
replace_ingredient(self.ingredients, replace, normal, n_string)
end
end
return self
end
Recipe.rep_ing = Recipe.replace_ingredient
-- Currently does no checking
function Recipe:clear_ingredients()
if self:is_valid() then
if self.normal then
if self.normal.ingredients then
self.normal.ingredients = {}
end
if self.expensive.ingredients then
self.expensive.ingredients = {}
end
elseif self.ingredients then
self.ingredients = {}
end
end
return self
end
--- Converts a recipe to the difficulty recipe format.
-- @tparam[opt] number expensive_energy crafting energy_required for the expensive recipe
-- @treturn self
function Recipe:make_difficult(expensive_energy)
if self:is_valid('recipe') and not self.normal then
--convert all ingredients
local normal, expensive = {}, {}
for _, ingredient in ipairs(self.ingredients) do
local this = format(ingredient)
normal[#normal + 1] = this
expensive[#expensive + 1] = Table.deepcopy(this)
end
local r_normal, r_expensive = {}, {}
for _, ingredient in ipairs(self.results or { self.result }) do
local this = format(ingredient)
r_normal[#r_normal + 1] = this
r_expensive[#r_expensive + 1] = Table.deepcopy(this)
end
self.normal = {
enabled = self.enabled,
energy_required = self.energy_required,
ingredients = normal,
results = r_normal,
main_product = self.main_product
}
self.expensive = {
enabled = self.enabled,
energy_required = expensive_energy or self.energy_required,
ingredients = expensive,
results = r_expensive,
main_product = self.main_product
}
self.ingredients = nil
self.result = nil
self.results = nil
self.result_count = nil
self.energy_required = nil
self.enabled = nil
self.main_product = nil
end
return self
end
--- Change the recipe category.
-- @tparam string category_name The new crafting category
-- @treturn self
function Recipe:change_category(category_name)
if self:is_valid() then
local Category = require('__stdlib__/stdlib/data/category')
self.category = Category(category_name, 'recipe-category'):is_valid() and category_name or self.category
end
return self
end
Recipe.set_category = Recipe.change_category
--- Add to technology as a recipe unlock.
-- @tparam string tech_name Name of the technology to add the unlock too
-- @treturn self
function Recipe:add_unlock(tech_name)
if self:is_valid() then
local Tech = require('__stdlib__/stdlib/data/technology')
Tech.add_effect(self, tech_name) --self is passed as a valid recipe
end
return self
end
--- Remove the recipe unlock from the technology.
-- @tparam string tech_name Name of the technology to remove the unlock from
-- @treturn self
function Recipe:remove_unlock(tech_name)
if self:is_valid('recipe') then
local Tech = require('__stdlib__/stdlib/data/technology')
Tech.remove_effect(self, tech_name, 'unlock-recipe')
end
return self
end
--- Set the enabled status of the recipe.
-- @tparam boolean enabled Enable or disable the recipe
-- @treturn self
function Recipe:set_enabled(enabled)
if self:is_valid() then
if self.normal then
self.normal.enabled = enabled
self.expensive.enabled = enabled
else
self.enabled = enabled
end
end
return self
end
--- Convert result type to results type.
-- @treturn self
function Recipe:convert_results()
if self:is_valid('recipe') then
if self.normal then
if self.normal.result then
self.normal.results = {
format(self.normal.result, self.normal.result_count or 1)
}
self.normal.result = nil
self.normal.result_count = nil
end
if self.expensive.result then
self.expensive.results = {
format(self.expensive.result, self.expensive.result_count or 1)
}
self.expensive.result = nil
self.expensive.result_count = nil
end
elseif self.result then
self.results = {
format(self.result, self.result_count or 1)
}
self.result = nil
self.result_count = nil
end
end
return self
end
--- Set the main product of the recipe.
-- @tparam string|boolean main_product if boolean then use normal/expensive recipes passed as main product
-- @tparam[opt] Concepts.Product|string normal recipe
-- @tparam[opt] Concepts.Product|string expensive recipe
-- @treturn self
function Recipe:set_main_product(main_product, normal, expensive)
if self:is_valid('recipe') then
normal, expensive = get_difficulties(normal, expensive)
local normal_main, expensive_main
if main_product then
local Item = require('__stdlib__/stdlib/data/item')
if type(main_product) == 'string' and Item(main_product):is_valid() then
normal_main = normal and main_product
expensive_main = expensive and main_product
elseif type(main_product) == 'boolean' then
normal_main = normal and Item(normal.name):is_valid() and normal.name
expensive_main = expensive and Item(expensive.name):is_valid() and expensive.name
end
if self.normal then
self.normal.main_product = normal_main
self.expensive.main_product = expensive_main
else
self.main_product = normal_main
end
end
end
return self
end
--- Remove the main product of the recipe.
-- @tparam[opt=false] boolean for_normal
-- @tparam[opt=false] boolean for_expensive
function Recipe:remove_main_product(for_normal, for_expensive)
if self:is_valid('recipe') then
if self.normal then
if for_normal or (for_normal == nil and for_expensive == nil) then
self.normal.main_product = nil
end
if for_expensive or (for_normal == nil and for_expensive == nil) then
self.expensive.main_product = nil
end
elseif for_normal or (for_normal == nil and for_expensive == nil) then
self.main_product = nil
end
end
return self
end
--- Add a new product to results, converts if needed.
-- @tparam string|Concepts.product normal
-- @tparam[opt] string|Concepts.product|boolean expensive
-- @tparam[opt] string main_product
function Recipe:add_result(normal, expensive, main_product)
if self:is_valid() then
normal, expensive = get_difficulties(normal, expensive)
self:convert_results()
self:set_main_product(main_product, normal, expensive)
-- if self.normal then
-- if normal then
-- end
-- if expensive then
-- end
-- elseif normal then
-- end
end
return self
end
--- Remove a product from results, converts if needed.
-- @tparam[opt] string|Concepts.product normal
-- @tparam[opt] string|Concepts.product|boolean expensive
-- @tparam[opt] string main_product new main_product to use
function Recipe:remove_result(normal, expensive, main_product)
if self:is_valid() then
normal, expensive = get_difficulties(normal, expensive)
self:convert_results()
self:set_main_product(main_product, normal, expensive)
-- if self.normal then
-- if normal then
-- end
-- if expensive then
-- end
-- elseif normal then
-- end
end
return self
end
--- Remove a product from results, converts if needed.
-- @tparam string|Concepts.product result_name
-- @tparam[opt] string|Concepts.product normal
-- @tparam[opt] string|Concepts.product|boolean expensive
-- @tparam[opt] string main_product
function Recipe:replace_result(result_name, normal, expensive, main_product)
if self:is_valid() and normal or expensive then
result_name = format(result_name)
if result_name then
normal, expensive = get_difficulties(normal, expensive)
self:convert_results()
self:remove_result(result_name, expensive and result_name)
self:set_main_product(main_product, normal, expensive)
-- if self.normal then
-- if normal then
-- end
-- if expensive then
-- end
-- elseif normal then
-- end
end
end
return self
end
return Recipe

View File

@@ -0,0 +1,160 @@
--- Technology
-- @classmod Data.Technology
local Data = require('__stdlib__/stdlib/data/data')
local Technology = {
__class = 'Technology',
__index = Data,
}
function Technology:__call(tech)
return self:get(tech, 'technology')
end
setmetatable(Technology, Technology)
function Technology:add_effect(effect, unlock_type)
assert(effect)
--todo fix for non recipe types
local add_unlock =
function(technology, name)
local effects = technology.effects
effects[#effects + 1] = {
type = unlock_type,
recipe = name
}
end
if self:is_valid('technology') then
local Recipe = require('__stdlib__/stdlib/data/recipe')
unlock_type = (not unlock_type and 'unlock-recipe') or unlock_type
local r_name = type(effect) == 'table' and effect.name or effect
if unlock_type == 'unlock-recipe' or not unlock_type then
if Recipe(effect):is_valid() then
add_unlock(self, r_name)
end
end
elseif self:is_valid('recipe') then
unlock_type = 'unlock-recipe'
-- Convert to array and return first valid tech
local techs = type(effect) == 'string' and { effect } or effect
for _, name in pairs(techs) do
local tech = Technology(name)
if tech:is_valid('technology') then
self:set_enabled(false)
add_unlock(tech, self.name)
break
end
end
end
return self
end
function Technology:remove_effect(tech_name, unlock_type, name)
if self:is_valid('technology') then
return self, name, unlock_type ---@todo implement
elseif self:is_valid('recipe') then
if tech_name then
local tech = Technology(tech_name)
if tech:is_valid() then
for index, effect in pairs(tech.effects or {}) do
if effect.type == 'unlock-recipe' and effect.recipe == self.name then
table.remove(tech.effects, index)
end
end
end
else
for _, tech in pairs(data.raw['technology']) do
for index, effect in pairs(tech.effects or {}) do
if effect.type == 'unlock-recipe' and effect.recipe == self.name then
table.remove(tech.effects, index)
end
end
end
end
end
return self
end
function Technology:add_pack(new_pack, count)
if self:is_valid('technology') then
local Item = require('__stdlib__/stdlib/data/item')
if type(new_pack) == 'table' then
count = new_pack[2] or 1
new_pack = new_pack[1]
elseif type(new_pack) == 'string' then
count = count or 1
else
error('new_pack must be a table or string')
end
if Item(new_pack):is_valid() then
self.unit.ingredients = self.unit.ingredients or {}
local ing = self.unit.ingredients
ing[#ing + 1] = { new_pack, count }
end
end
return self
end
function Technology:remove_pack(pack)
if self:is_valid('technology') then
local ings = self.unit.ingredients
for i, ing in pairs(ings or {}) do
if ing[1] == pack then
table.remove(ings, i)
break
end
end
end
return self
end
function Technology:replace_pack(old_pack, new_pack, count)
if self:is_valid('technology') then
local ings = self.unit.ingredients
for i, ing in pairs(ings or {}) do
if ing[1] == old_pack then
ing[1] = new_pack
ing[2] = count or ing[2] or 1
break
end
end
end
return self
end
function Technology:add_prereq(tech_name)
if self:is_valid('technology') and Technology(tech_name):is_valid() then
self.prerequisites = self.prerequisites or {}
local pre = self.prerequisites
for _, existing in pairs(pre) do
if existing == tech_name then
return self
end
end
pre[#pre + 1] = tech_name
end
return self
end
function Technology:remove_prereq(tech_name)
if self:is_valid('technology') then
local pre = self.prerequisites or {}
for i = #pre, 1, -1 do
if pre[i] == tech_name then
table.remove(pre, i)
break
end
end
if #pre == 0 then
self.prerequisites = nil
end
end
return self
end
return Technology

View File

@@ -0,0 +1,184 @@
--- Tools for working with entities.
-- @module Entity.Entity
-- @usage local Entity = require('__stdlib__/stdlib/entity/entity')
local Entity = {
__class = 'Entity',
__index = require('__stdlib__/stdlib/core')
}
setmetatable(Entity, Entity)
--- Tests whether an entity has access to a given field.
-- @tparam LuaEntity entity the entity to test the access to a field
-- @tparam string field_name the field name
-- @treturn boolean true if the entity has access to the field, false if the entity threw an exception when trying to access the field
function Entity.has(entity, field_name)
assert(entity, 'missing entity argument')
assert(field_name, 'missing field name argument')
local status =
pcall(
function()
return entity[field_name]
end
)
return status
end
--- Gets the user data that is associated with an entity.
-- The user data is stored in the global object and it persists between loads.
--> The user data will be removed from an entity when the entity becomes invalid.
-- @tparam LuaEntity entity the entity to look up
-- @treturn ?|nil|Mixed the user data, or nil if no data exists for the entity
function Entity.get_data(entity)
assert(entity, 'missing entity argument')
if not global._entity_data then
return nil
end
local unit_number = entity.unit_number
if unit_number then
return global._entity_data[unit_number]
else
local entity_name = entity.name
if not global._entity_data[entity_name] then
return nil
end
local entity_category = global._entity_data[entity_name]
for _, entity_data in pairs(entity_category) do
if Entity._are_equal(entity_data.entity, entity) then
return entity_data.data
end
end
return nil
end
end
--- Associates the user data to an entity.
-- The user data will be stored in the global object and it will persist between loads.
--> The user data will be removed from an entity when the entity becomes invalid.
-- @tparam LuaEntity entity the entity with which to associate the user data
-- @tparam ?|nil|Mixed data the data to set, or nil to delete the data associated with the entity
-- @treturn ?|nil|Mixed the previous data associated with the entity, or nil if the entity had no previous data
function Entity.set_data(entity, data)
assert(entity, 'missing entity argument')
if not global._entity_data then
global._entity_data = {}
end
local unit_number = entity.unit_number
if unit_number then
local prev = global._entity_data[unit_number]
global._entity_data[unit_number] = data
return prev
else
local entity_name = entity.name
if not global._entity_data[entity_name] then
global._entity_data[entity_name] = {}
end
local entity_category = global._entity_data[entity_name]
for i = #entity_category, 1, -1 do
local entity_data = entity_category[i]
if not entity_data.entity.valid then
table.remove(entity_category, i)
end
if Entity._are_equal(entity_data.entity, entity) then
local prev = entity_data.data
if data then
entity_data.data = data
else
table.remove(entity_category, i)
end
return prev
end
end
table.insert(entity_category, { entity = entity, data = data })
end
return nil
end
--- Freezes an entity, by making it inactive, inoperable, and non-rotatable, or unfreezes by doing the reverse.
-- @tparam LuaEntity entity the entity to freeze or unfreeze
-- @tparam[opt=true] boolean mode if true, freezes the entity, if false, unfreezes the entity. If not specified, it is set to true
-- @treturn LuaEntity the entity that has been frozen or unfrozen
function Entity.set_frozen(entity, mode)
assert(entity, 'missing entity argument')
mode = mode == false and true or false
entity.active = mode
entity.operable = mode
entity.rotatable = mode
return entity
end
--- Makes an entity indestructible so that it cannot be damaged or mined neither by the player nor by their enemy factions.
-- @tparam LuaEntity entity the entity to make indestructable
-- @tparam[opt=true] boolean mode if true, makes the entity indestructible, if false, makes the entity destructable
-- @treturn LuaEntity the entity that has been made indestructable or destructable
function Entity.set_indestructible(entity, mode)
assert(entity, 'missing entity argument')
mode = mode == false and true or false
entity.minable = mode
entity.destructible = mode
return entity
end
--- Tests if two entities are equal.
-- If they don't have a reference equality and ***entity\_a*** has ***equals*** function, it will be called with ***entity\_b*** as its first argument.
-- @tparam LuaEntity entity_a
-- @tparam LuaEntity entity_b
-- @treturn boolean
function Entity._are_equal(entity_a, entity_b)
if entity_a == nil then
return entity_a == entity_b
elseif entity_a == entity_b then
return true
elseif Entity.has(entity_a, 'equals') and entity_a.equals ~= nil then
return entity_a.equals(entity_b)
else
return false
end
end
function Entity.find_resources(entity, all)
if entity.type == 'mining-drill' then
local radius = entity.prototype.mining_drill_radius
local name = (not all and (entity.mining_target and entity.mining_target.name)) or nil
return entity.surface.count_entities_filtered {
type = 'resource',
name = name,
position = entity.position,
radius = radius,
}
end
return 0
end
function Entity.is_damaged(entity)
return entity.get_health_ratio() < 1
end
Entity.damaged = Entity.is_damaged
function Entity.is_circuit_connected(entity)
local list = entity.circuit_connected_entities
return list and (next(list.red) or next(list.green))
end
function Entity.count_circuit_connections(entity)
local list = entity.circuit_connected_entities
return list and #list.red + #list.green
end
function Entity.has_fluidbox(entity)
local box = entity.fluidbox
return box and #box > 0
end
function Entity.can_deconstruct(entity)
return entity.minable and entity.prototype.selectable_in_game and not entity.has_flag('not-deconstructable')
end
return Entity

View File

@@ -0,0 +1,157 @@
--- For working with inventories.
-- @module Entity.Inventory
-- @usage local Inventory = require('__stdlib__/stdlib/entity/inventory')
local Inventory = {
__class = 'Inventory',
__index = require('__stdlib__/stdlib/core')
}
setmetatable(Inventory, Inventory)
local min = math.min
--- Given a function, apply it to each slot in the given inventory.
-- Passes the index of a slot as the second argument to the given function.
-- <p>Iteration is aborted if the applied function returns true for any element during iteration.
-- @tparam LuaInventory inventory the inventory to iterate
-- @tparam function func the function to apply to values
-- @param[opt] ... additional arguments passed to the function
-- @treturn ?|nil|LuaItemStack the slot where the iteration was aborted **OR** nil if not aborted
function Inventory.each(inventory, func, ...)
local index
for i = 1, #inventory do
if func(inventory[i], i, ...) then
index = i
break
end
end
return index and inventory[index]
end
--- Given a function, apply it to each slot in the given inventory.
-- Passes the index of a slot as the second argument to the given function.
-- <p>Iteration is aborted if the applied function returns true for any element during iteration.
-- <p>Iteration is performed from last to first in order to support dynamically sized inventories.
-- @tparam LuaInventory inventory the inventory to iterate
-- @tparam function func the function to apply to values
-- @param[opt] ... additional arguments passed to the function
-- @treturn ?|nil|LuaItemStack the slot where the iteration was aborted **OR** nil if not aborted
function Inventory.each_reverse(inventory, func, ...)
local index
for i = #inventory, 1, -1 do
if func(inventory[i], i, ...) then
index = i
break
end
end
return index and inventory[index]
end
--- Copies the contents of source inventory to destination inventory by using @{Concepts.SimpleItemStack}.
-- @tparam LuaInventory src the source inventory
-- @tparam LuaInventory dest the destination inventory
-- @tparam[opt=false] boolean clear clear the contents of the source inventory
-- @treturn {Concepts.SimpleItemStack,...} an array of left over items that could not be inserted into the destination
function Inventory.copy_as_simple_stacks(src, dest, clear)
assert(src, 'missing source inventory')
assert(dest, 'missing destination inventory')
local left_over = {}
for i = 1, #src do
local stack = src[i]
if stack and stack.valid and stack.valid_for_read then
local simple_stack = {
name = stack.name,
count = stack.count,
health = stack.health or 1,
durability = stack.durability
}
-- ammo is a special case field, accessing it on non-ammo itemstacks causes an exception
simple_stack.ammo = stack.prototype.magazine_size and stack.ammo
--Insert simple stack into inventory, add to left_over if not all were inserted.
simple_stack.count = simple_stack.count - dest.insert(simple_stack)
if simple_stack.count > 0 then
table.insert(left_over, simple_stack)
end
end
end
if clear then
src.clear()
end
return left_over
end
--- Return a blueprint stack from either stack or blueprint_book
-- @tparam LuaItemStack stack
-- @tparam[opt] bool is_bp_setup
-- @tparam[opt] bool no_book
-- @treturn LuaItemStack
function Inventory.get_blueprint(stack, is_bp_setup, no_book)
if stack and stack.valid and stack.valid_for_read then
if stack.is_blueprint then
return not is_bp_setup and stack or stack.is_blueprint_setup() and stack
elseif stack.is_blueprint_book and not no_book and stack.active_index then
local book = stack.get_inventory(defines.inventory.item_main)
if #book >= stack.active_index then
return Inventory.get_blueprint(book[stack.active_index], is_bp_setup)
end
end
end
end
--- Is the stack a blueprint with label?
-- @tparam LuaItemStack stack
-- @tparam string label
-- @treturn bool
function Inventory.is_named_bp(stack, label)
return stack and stack.valid_for_read and stack.is_blueprint and stack.label and stack.label:find('^' .. label)
end
--- Returns either the item at a position, or the filter at the position if there isn't an item there.
-- @tparam LuaInventory inventory
-- @tparam int idx
-- @tparam[opt] bool item_only
-- @tparam[opt] bool filter_only
-- @return the item or filter
function Inventory.get_item_or_filter(inventory, idx, item_only, filter_only)
local filter = not item_only and inventory.get_filter(idx)
return filter or (not filter_only and inventory[idx].valid_for_read and inventory[idx].name) or nil
end
--- Transfer items from 1 inventory to another.
-- @tparam LuaInventory source
-- @tparam LuaInventory destination
-- @tparam[opt=nil] table source_filters the filters to use if the source is not filtered/filterable
-- @treturn nil|table the filters if the destination does not support filters
function Inventory.transfer_inventory(source, destination, source_filters)
local filtered = source.is_filtered()
local destination_filterable = destination.supports_filters()
local filters = {}
for i = 1, min(#destination, #source) do
destination[i].transfer_stack(source[i])
if filtered then
if destination_filterable then
destination.set_filter(i, source.get_filter(i))
else
filters[i] = source.get_filter(i)
end
elseif source_filters then
if destination_filterable then
destination.set_filter(i, source_filters[i])
end
end
end
return (filtered and not destination_filterable and filters) or nil
end
--- Swap items from 1 inventory to another.
-- @tparam LuaInventory source
-- @tparam LuaInventory destination
function Inventory.swap_inventory(source, destination)
for i = 1, min(#destination, #source) do
destination[i].swap_stack(source[1])
end
end
return Inventory

View File

@@ -0,0 +1,204 @@
--- Resource utilities.
-- @module Entity.Resource
-- @usage local Resource = require('__stdlib__/stdlib/entity/resource')
local Resource = { __class = 'Resource',
__index = require('__stdlib__/stdlib/core') }
setmetatable(Resource, Resource)
local Is = require('__stdlib__/stdlib/utils/is')
local Surface = require('__stdlib__/stdlib/area/surface')
local Area = require('__stdlib__/stdlib/area/area')
local Tile = require('__stdlib__/stdlib/area/tile')
local Queue = require('__stdlib__/stdlib/misc/queue')
local table = require('__stdlib__/stdlib/utils/table')
--- Gets all resource entities at the specified position and surface.
-- Adapted from *YARM/resmon.lua &rarr; find\_resource\_at*
-- @tparam string|LuaSurface surface the surface to look up
-- @tparam Concepts.Position position the position to check
-- @treturn {nil|LuaEntity,...} an array of resource entities or nil if none found
function Resource.get_resources_at(surface, position)
Is.Assert(surface, 'missing surface')
Is.Assert(position, 'missing position')
local surfaces = Surface.lookup(surface)
Is.Assert(#surfaces == 1, 'invalid surface')
local tile_at_position = Tile.from_position(Tile(position))
local tile_area = Tile.to_area(tile_at_position)
local resources_at_tile = table.first(surfaces).find_entities_filtered { area = tile_area, type = 'resource' } or {}
return resources_at_tile
end
--- From the resources at the given surface and position, return all connected (horizontally, vertically and diagonally) resource entities.
-- <p>When the resource patches are found, the returned object will be an associative array where the key is the
-- resource-type string and the value is an array of entities that correspond to the resource-type.
-- <p>For now, this function gets just the ore patches, since problems arise when a single resource entity spans multiple tiles.
--> This implementation is unstable; if a resource entity reference changes during the search,
-- *both the old and the new version* of the entity might be included.
-- @tparam LuaSurface surface the surface to look up
-- @tparam Concepts.Position position the position to check
-- @return (<span class="types">{@{nil}} or {[@{string} &lt;resource-type&gt;] = {@{LuaEntity},...},...}</span>)
-- a map of resource types to resource entities or empty array if they don't exist
function Resource.get_resource_patches_at(surface, position)
Is.Assert(surface, 'missing surface')
Is.Assert(position, 'missing position')
-- get the initial resource tile if there is one at the given position
local all_resource_entities = Resource.get_resources_at(surface, position)
local all_resource_types = Resource.get_resource_types(all_resource_entities)
local resource_patches = {}
for _, type in pairs(all_resource_types) do
local resource_patch = Resource.get_resource_patch_at(surface, position, type)
resource_patches[type] = resource_patch
end
return resource_patches
end
--- From the resources at the given surface and position, return all connected (horizontally, vertically and diagonally) resource entities of specified type.
-- <p>For now, this function gets just the ore patches, since problems arise when a single resource entity spans multiple tiles.
--> This implementation is unstable; if a resource entity reference changes during the search,
-- *both the old and the new version* of the entity might be included.
-- @tparam LuaSurface surface the surface to look up
-- @tparam Concepts.Position position the position to check
-- @tparam string type the resource type (example: "iron-ore")
-- @treturn {nil|LuaEntity,...} an array containing all resources in the resource patch, or an empty array if there are no resources there
function Resource.get_resource_patch_at(surface, position, type)
Is.Assert(surface, 'missing surface')
Is.Assert(position, 'missing position')
Is.Assert(position, 'missing ore name')
local surfaces = Surface.lookup(surface)
Is.Assert(#surfaces == 1, 'invalid surface')
surface = table.first(surfaces)
-- get the initial resource tile if there is one at the given position
local all_resource_entities = Resource.get_resources_at(surface, position)
local filtered_resource_entities = Resource.filter_resources(all_resource_entities, { type })
if #filtered_resource_entities == 0 then
return {}
end
-- for the search, keep track of the relevant entities and tiles visited
-- we use the entities as keys to prevent having a single entity multiple times in the list
local resource_patch = {}
local visited_tiles = {}
-- local cache of bitwise functions, because they are called in a tight loop
local bitwise_or = bit32.bor
local bitwise_and = bit32.band
local bitwise_lshift = bit32.lshift
local initial_tile = Tile.from_position(Tile(filtered_resource_entities[1].position))
-- do a BFS starting from the initial tile
local search_queue = Queue.new()
Queue.push_last(search_queue, initial_tile)
while not Queue.is_empty(search_queue) do
local current_tile = Queue.pop_first(search_queue) --[[@as MapPosition Fix when typed]]
local current_entities = surface.find_entities_filtered { area = Tile.to_area(current_tile), type = 'resource' }
local current_tile_index = bitwise_or(bitwise_lshift(bitwise_and(current_tile.x, 0xFFFF), 16), bitwise_and(current_tile.y, 0xFFFF))
visited_tiles[current_tile_index] = true
local filtered_current_entities = Resource.filter_resources(current_entities, { type })
if #filtered_current_entities ~= 0 then
-- this tile belongs to the ore patch, add the resources
table.merge(resource_patch, table.invert(filtered_current_entities))
-- queue all tiles around this one that we did not visit yet
for _, adjacent_tile in pairs(Tile.adjacent(surface, current_tile, true)) do
local adj_tile_index = bitwise_or(bitwise_lshift(bitwise_and(adjacent_tile.x, 0xFFFF), 16), bitwise_and(adjacent_tile.y, 0xFFFF))
if not visited_tiles[adj_tile_index] then
Queue.push_last(search_queue, adjacent_tile)
visited_tiles[adj_tile_index] = true
end
end
end
end
-- map the resource entities back to an array
resource_patch = table.keys(resource_patch)
return resource_patch
end
--- Given an array of resource entities, get an array containing their names.
-- Every element within the new array is unique and is the name of a resource entity.
-- @tparam {LuaEntity,...} resources an array of resource entities
-- @treturn {nil|string,...} a new array with the names of the resources or nil if no resource entities are given
function Resource.get_resource_types(resources)
local result = {}
if resources then
local resource_names = {}
for _, resource in pairs(resources) do
resource_names[resource.name] = true
end
result = table.keys(resource_names, false, true)
end
return result
end
--- Given an array of resource entities, return the ones that have the given resource names.
-- @tparam {LuaEntity,...} resources an array of resource entities
-- @tparam {string,...} resource_names the names of the resource entities
-- @treturn {nil|LuaEntity,...} a new array containing the entities matching the given resource names or nil if no matches were found
function Resource.filter_resources(resources, resource_names)
Is.Assert(resources, 'missing resource entities list')
if not resource_names or #resource_names == 0 then
return resources
end
-- filter the resources that have the same name as one of the given names in resource_names
local result =
table.filter(
resources,
function(resource_entity)
return table.any(
resource_names,
function(name)
return resource_entity.name == name
end
)
end
)
return result
end
--- Given a resource patch, return its area.
-- @see Resource.get_resource_patch_at
-- @tparam {LuaEntity,...} resource_patch the resource patch
-- @treturn Concepts.BoundingBox the area of the resource patch
function Resource.get_resource_patch_bounds(resource_patch)
Is.Assert(resource_patch, 'missing resource patch')
local min_x = math.huge
local min_y = math.huge
local max_x = -math.huge
local max_y = -math.huge
for _, entity in pairs(resource_patch) do
local pos = entity.position
min_x = math.min(min_x, pos.x)
min_y = math.min(min_y, pos.y)
max_x = math.max(max_x, pos.x)
max_y = math.max(max_y, pos.y)
end
return Area.construct(min_x, min_y, max_x, max_y)
end
return Resource

View File

@@ -0,0 +1,153 @@
--- Configuration changed event handling.
-- This module registers events
-- @module Event.Changes
-- @usage
-- local Changes = require('__stdlib__/stdlib/event/changes')
-- Changes.register('mod_versions', 'path_to_version_file')
-- @usage
-- -- version files should return a dictionary of functions indexed by version number.
-- return {['1.0.0'] = function() end}
-- @usage
-- -- Other change files should return a single function and will run in the order they are added.
-- -- Multiple files can be registered to a change type.
-- Changes.register('any-first', 'path_to_file_1')
-- Changes.register('any-first', 'path_to_file_2')
local Event = require('__stdlib__/stdlib/event/event')
---@class Changes
---@field map_first table
---@field any_first table
---@field mod_first table
---@field mod_versions table
---@field mod_last table
---@field any_last table
---@field map_last table
---@field get_file_path function
local Changes = {
__class = 'Changes',
__index = require('__stdlib__/stdlib/core'),
registered_for_events = false
}
setmetatable(Changes, Changes)
local inspect = _ENV.inspect
--[[
ConfigurationChangedData
Table with the following fields:
old_version :: string (optional): Old version of the map. Present only when loading map version other than the current version.
new_version :: string (optional): New version of the map. Present only when loading map version other than the current version.
mod_changes :: dictionary string → ModConfigurationChangedData: Dictionary of mod changes. It is indexed by mod name.
ModConfigurationChangedData
Table with the following fields:
old_version :: string: Old version of the mod. May be nil if the mod wasn't previously present (i.e. it was just added).
new_version :: string: New version of the mod. May be nil if the mod is no longer present (i.e. it was just removed).
--]]
local table = require('__stdlib__/stdlib/utils/table')
local map_changes = {
['map_first'] = true,
['any_first'] = true,
['mod_first'] = true,
['mod_versions'] = true,
['mod_last'] = true,
['any_last'] = true,
['map_last'] = true
}
for change_type in pairs(map_changes) do
Changes[change_type] = {}
end
local function run_if_exists(path)
for _, fun in pairs(path) do
if type(fun) == 'function' then
fun()
end
end
end
function Changes.register_events(change_type, path)
if map_changes[change_type] then
if not Changes.registered_for_events then
Event.register(Event.core_events.configuration_changed, Changes.on_configuration_changed)
if change_type == 'mod_versions' then
-- Register on_init only for mod_versions changes
Event.register(Event.core_events.init, Changes.on_init)
end
end
Changes[change_type][path] = require(path)
else
error('Incorrect change type ' .. (change_type or 'nil') .. ' expected: ' .. table.concat(table.keys(map_changes), ', ') .. '.')
end
return Changes
end
Changes.register = Changes.register_events
function Changes.register_versions(path)
return Changes.register_events('mod_versions', path)
end
-- Mark all version changes as complete during Init
function Changes.on_init()
for _, versions in pairs(Changes.mod_versions) do
local list = {}
local cur_version = game.active_mods[script.mod_name]
for ver in pairs(versions) do
list[ver] = cur_version
end
global._changes = list
end
end
function Changes.on_configuration_changed(event)
run_if_exists(Changes.map_first)
if event.mod_changes then
run_if_exists(Changes.any_first)
if event.mod_changes[script.mod_name] then
run_if_exists(Changes.mod_first)
local this_mod_changes = event.mod_changes[script.mod_name]
Changes.on_mod_changed(this_mod_changes)
log(script.mod_name .. ': version changed from ' .. tostring(this_mod_changes.old_version) .. ' to ' .. tostring(this_mod_changes.new_version))
run_if_exists(Changes.mod_last)
end
run_if_exists(Changes.any_last)
end
run_if_exists(Changes.map_last)
end
function Changes.on_mod_changed(this_mod_changes)
global._changes = global._changes or {}
local old = this_mod_changes.old_version
if old then -- Find the last installed version
local versions = {}
for _, path in pairs(Changes.mod_versions) do
for ver, fun in pairs(path) do
if not global._changes[ver] then
versions[ver] = this_mod_changes.new_version
fun()
log('Migration completed for version ' .. ver)
end
end
end
table.each(
versions,
function(v, k)
global._changes[k] = v
end
)
end
end
function Changes.dump_data()
for change_type in pairs(map_changes) do
if table.size(Changes[change_type]) > 0 then
game.write_file(Changes.get_file_path('Changes/' .. change_type .. '.lua'),
'return ' .. inspect(Changes[change_type], { longkeys = true, arraykeys = true }))
end
end
game.write_file(Changes.get_file_path('Changes/global.lua'), 'return ' .. inspect(global._changes or nil, { longkeys = true, arraykeys = true }))
end
return Changes

View File

@@ -0,0 +1,529 @@
--- Makes working with events in Factorio a lot more simple.
-- <p>By default, Factorio allows you to register **only one handler** to an event.
-- <p>This module lets you easily register **multiple handlers** to an event.
-- <p>Using this module is as simple as replacing @{LuaBootstrap.on_event|script.on_event} with @{Event.register}.
-- <blockquote>
-- Due to the way that Factorio's event system works, it is not recommended to intermingle `script.on_event` and `Event.register` in a mod.
-- <br>This module hooks into Factorio's event system, and using `script.on_event` for the same event will change which events are registered.
-- </blockquote>
-- <blockquote>
-- This module does not have many of the multiplayer protections that `script.on_event` does.
-- <br>Due to this, great care should be taken when registering events conditionally.
-- </blockquote>
-- @module Event.Event
-- @usage local Event = require('__stdlib__/stdlib/event/event')
local config = require('__stdlib__/stdlib/config')
config.control = true
local Event = {
__class = 'Event',
registry = {}, -- Holds registered events
custom_events = {}, -- Holds custom event ids
stop_processing = {}, -- just has to be unique
Filters = require('__stdlib__/stdlib/event/modules/event_filters'),
__index = require('__stdlib__/stdlib/core')
}
setmetatable(Event, Event)
Event.options = {
protected_mode = false,
skip_valid = false,
force_crc = false -- Requires debug_mode to be true
}
local Event_options_meta = { __index = Event.options }
Event.core_events = {
on_init = 'on_init',
on_load = 'on_load',
on_configuration_changed = 'on_configuration_changed',
init = 'on_init',
load = 'on_load',
configuration_changed = 'on_configuration_changed',
init_and_config = { 'on_init', 'on_configuration_changed' },
init_and_load = { 'on_init', 'on_load' }
}
Event.script = {
on_event = script.on_event,
on_nth_tick = script.on_nth_tick,
on_init = script.on_init,
on_load = script.on_load,
on_configuration_changed = script.on_configuration_changed,
generate_event_name = script.generate_event_name,
get_event_handler = script.get_event_handler
}
local Type = require('__stdlib__/stdlib/utils/type')
local table = require('__stdlib__/stdlib/utils/table')
local assert, type, tonumber = assert, type, tonumber
local event_names = table.invert(defines.events)
if not config.skip_script_protections then -- Protections for post and pre registrations
for _, define in pairs(defines.events) do
if Event.script.get_event_handler(define) then
error('Detected attempt to add the STDLIB event module after using script.on_event')
end
end
for name in pairs(Event.script) do
_G.script[name] = function()
error('Detected attempt to register an event using script.' .. name .. ' while using the STDLIB event system ')
end
end
end
local bootstrap_events = {
on_init = function()
Event.dispatch { name = 'on_init' }
end,
on_load = function()
Event.dispatch { name = 'on_load', tick = -1 }
end,
on_configuration_changed = function(event)
event.name = 'on_configuration_changed'
Event.dispatch(event)
end
}
local function valid_id(id)
local id_type = type(id)
return (id_type == 'number' or id_type == 'string'), 'Invalid Event Id, Must be string/int/defines.events, Passed in: ' .. type(id)
end
local function valid_event_id(id)
return (tonumber(id) and id >= 0) or (Type.String(id) and not bootstrap_events[id])
end
local function id_to_name(name)
return event_names[name] or table.invert(Event.custom_events)[name] or name or 'unknown'
end
local stupid_events = {
[defines.events.script_raised_revive] = 'entity',
[defines.events.script_raised_built] = 'entity',
[defines.events.on_entity_cloned] = 'destination'
}
--- Registers a handler for the given events.
-- If a `nil` handler is passed, remove the given events and stop listening to them.
-- <p>Events dispatch in the order they are registered.
-- <p>An *event ID* can be obtained via @{defines.events},
-- @{LuaBootstrap.generate_event_name|script.generate_event_name} which is in <span class="types">@{int}</span>,
-- and can be a custom input name which is in <span class="types">@{string}</span>.
-- <p>The `event_id` parameter takes in either a single, multiple, or mixture of @{defines.events}, @{int}, and @{string}.
-- @usage
-- -- Create an event that prints the current tick every tick.
-- Event.register(defines.events.on_tick, function(event) game.print(event.tick) end)
-- -- Register something for Nth tick using negative numbers.
-- Event.register(-120, function() game.print('Every 120 ticks') end
-- -- Function call chaining
-- Event.register(event1, handler1).register(event2, handler2)
-- @param event_id (<span class="types">@{defines.events}, @{int}, @{string}, or {@{defines.events}, @{int}, @{string},...}</span>)
-- @tparam function handler the function to call when the given events are triggered
-- @tparam[opt=nil] function filter a function whose return determines if the handler is executed. event and pattern are passed into this
-- @tparam[opt=nil] mixed pattern an invariant that can be used in the filter function, passed as the second parameter to your filter
-- @tparam[opt=nil] table options a table of options that take precedence over the module options.
-- @return (<span class="types">@{Event}</span>) Event module object allowing for call chaining
function Event.register(event_id, handler, filter, pattern, options)
assert(event_id, 'missing event_id argument')
assert(Type.Function(handler), 'handler function is missing, use Event.remove to un register events')
assert(filter == nil or Type.Function(filter), 'filter must be a function when present')
assert(options == nil or Type.Table(options), 'options must be a table when present')
options = setmetatable(options or {}, Event_options_meta)
--Recursively handle event id tables
if Type.Table(event_id) then
for _, id in pairs(event_id) do
Event.register(id, handler)
end
return Event
end
assert(valid_id(event_id), 'event_id is invalid')
-- If the event_id has never been registered before make sure we call the correct script action to register
-- our Event handler with factorio
if not Event.registry[event_id] then
Event.registry[event_id] = {}
if Type.String(event_id) then
--String event ids will either be Bootstrap events or custom input events
if bootstrap_events[event_id] then
Event.script[event_id](bootstrap_events[event_id])
else
Event.script.on_event(event_id, Event.dispatch)
end
elseif event_id >= 0 then
--Positive values will be defines.events
Event.script.on_event(event_id, Event.dispatch)
elseif event_id < 0 then
--Use negative values to register on_nth_tick
Event.script.on_nth_tick(math.abs(event_id)--[[@as uint]] , Event.dispatch)
end
end
local registry = Event.registry[event_id]
--If handler is already registered for this event: remove it for re-insertion at the end.
if #registry > 0 then
for i, registered in ipairs(registry) do
if registered.handler == handler and registered.pattern == pattern and registered.filter == filter then
table.remove(registry, i)
local output = {
'__' .. script.mod_name .. '__',
' Duplicate handler registered for event ',
event_id .. '(' .. (event_names[event_id] or ' ') .. ')',
' at position ' .. i,
', moving it to the bottom.'
}
log(table.concat(output))
break
end
end
end
--Finally insert the handler
table.insert(registry, { handler = handler, filter = filter, pattern = pattern, options = options })
return Event
end
--- Removes a handler from the given events.
-- <p>When the last handler for an event is removed, stop listening to that event.
-- <p>An *event ID* can be obtained via @{defines.events},
-- @{LuaBootstrap.generate_event_name|script.generate_event_name} which is in <span class="types">@{int}</span>,
-- and can be a custom input name which is in <span class="types">@{string}</span>.
-- <p>The `event_id` parameter takes in either a single, multiple, or mixture of @{defines.events}, @{int}, and @{string}.
-- @param event_id (<span class="types">@{defines.events}, @{int}, @{string}, or {@{defines.events}, @{int}, @{string},...}</span>)
-- @tparam[opt] function handler the handler to remove, if not present remove all registered handlers for the event_id
-- @tparam[opt] function filter
-- @tparam[opt] mixed pattern
-- @return (<span class="types">@{Event}</span>) Event module object allowing for call chaining
function Event.remove(event_id, handler, filter, pattern)
assert(event_id, 'missing event_id argument')
-- Handle recursion here
if Type.Table(event_id) then
for _, id in pairs(event_id) do
Event.remove(id, handler)
end
return Event
end
assert(valid_id(event_id), 'event_id is invalid')
local registry = Event.registry[event_id]
if registry then
local found_something = false
for i = #registry, 1, -1 do
local registered = registry[i]
if handler then -- handler, possibly filter, possibly pattern
if handler == registered.handler then
if not filter and not pattern then
table.remove(registry, i)
found_something = true
elseif filter then
if filter == registered.filter then
if not pattern then
table.remove(registry, i)
found_something = true
elseif pattern and pattern == registered.pattern then
table.remove(registry, i)
found_something = true
end
end
elseif pattern and pattern == registered.pattern then
table.remove(registry, i)
found_something = true
end
end
elseif filter then -- no handler, filter, possibly pattern
if filter == registered.filter then
if not pattern then
table.remove(registry, i)
found_something = true
elseif pattern and pattern == registered.pattern then
table.remove(registry, i)
found_something = true
end
end
elseif pattern then -- no handler, no filter, pattern
if pattern == registered.pattern then
table.remove(registry, i)
found_something = true
end
else -- no handler, filter, or pattern
table.remove(registry, i)
found_something = true
end
end
if found_something and table.size(registry) == 0 then
-- Clear the registry data and un subscribe if there are no registered handlers left
Event.registry[event_id] = nil
if Type.String(event_id) then
-- String event ids will either be Bootstrap events or custom input events
if bootstrap_events[event_id] then
Event.script[event_id](nil)
else
Event.script.on_event(event_id, nil)
end
elseif event_id >= 0 then
-- Positive values will be defines.events
Event.script.on_event(event_id, nil)
elseif event_id < 0 then
-- Use negative values to remove on_nth_tick
Event.script.on_nth_tick(math.abs(event_id)--[[@as uint]] , nil)
end
elseif not found_something then
log('Attempt to deregister already non-registered listener from event: ' .. event_id)
end
else
log('Attempt to deregister already non-registered listener from event: ' .. event_id)
end
return Event
end
--- Shortcut for `Event.register(Event.core_events.on_load, function)`
-- @return (<span class="types">@{Event}</span>) Event module object allowing for call chaining
function Event.on_load(...)
return Event.register(Event.core_events.on_load, ...)
end
function Event.on_load_if(truthy, ...)
if truthy then
return Event.on_load(...)
end
return Event
end
--- Shortcut for `Event.register(Event.core_events.on_configuration_changed, function)`
-- @return (<span class="types">@{Event}</span>) Event module object allowing for call chaining
function Event.on_configuration_changed(...)
return Event.register(Event.core_events.on_configuration_changed, ...)
end
--- Shortcut for `Event.register(Event.core_events.on_init, function)`
-- @return (<span class="types">@{Event}</span>) Event module object allowing for call chaining
function Event.on_init(...)
return Event.register(Event.core_events.on_init, ...)
end
function Event.on_init_if(truthy, ...)
if truthy then
return Event.on_init(...)
end
return Event
end
--- Shortcut for `Event.register(-nthTick, function)`
-- @return (<span class="types">@{Event}</span>) Event module object allowing for call chaining
function Event.on_nth_tick(nth_tick, ...)
return Event.register(-math.abs(nth_tick), ...)
end
--- Shortcut for `Event.register(defines.events, function)`
-- @function Event.on_event
-- @return (<span class="types">@{Event}</span>) Event module object allowing for call chaining
Event.on_event = Event.register
function Event.register_if(truthy, id, ...)
if truthy then
return Event.register(id, ...)
end
return Event
end
Event.on_event_if = Event.register_if
-- Used to replace pcall in un-protected events.
local function no_pcall(handler, ...)
return true, handler(...)
end
-- A dispatch helper function
-- Call any filter and as applicable the event handler.
-- protected errors are logged to game console if game is available, otherwise a real error
-- is thrown. Bootstrap events are not protected from erroring no matter the option.
local function dispatch_event(event, registered)
local success, match_result, handler_result
local protected = event.options.protected_mode
local pcall = not bootstrap_events[event.name] and protected and pcall or no_pcall
-- If we have a filter run it first passing event, and registered.pattern as parameters
-- If the filter returns truthy call the handler passing event, and the result from the filter
if registered.filter then
success, match_result = pcall(registered.filter, event, registered.pattern)
if success and match_result then
success, handler_result = pcall(registered.handler, event, match_result)
end
else
success, handler_result = pcall(registered.handler, event, registered.pattern)
end
-- If the handler errors lets make sure someone notices
if not success and not Event.log_and_print(handler_result or match_result) then
-- no players received the message, force a real error so someone notices
error(handler_result or match_result)
end
return success and handler_result or nil
end
--- The user should create a table in this format, for a table that will be passed into @{Event.dispatch}.
-- <p>In general, the user should create an event data table that is in a similar format as the one that Factorio returns.
--> The event data table **MUST** have either `name` or `input_name`.
-- @tfield[opt] int|defines.events name unique event ID generated with @{LuaBootstrap.generate_event_name|script.generate_event_name} ***OR*** @{defines.events}
-- @tfield[opt] string input_name custom input name of an event
-- @field[opt] ... any # of additional fields with extra data, which are passed into the handler registered to an event that this table represents
-- @usage
-- -- below code is from Trains module.
-- -- old_id & new_id are additional fields passed into the handler that's registered to Trains.on_train_id_changed event.
-- local event_data = {
-- old_id = renaming.old_id,
-- new_id = renaming.new_id,
-- name = Trains.on_train_id_changed
-- }
-- Event.dispatch(event_data)
-- @table event_data
--- Calls the handlers that are registered to the given event.
-- <p>Abort calling remaining handlers if any one of them has invalid userdata.
-- <p>Handlers are dispatched in the order they were created.
-- @param event (<span class="types">@{event_data}</span>) the event data table
-- @see https://forums.factorio.com/viewtopic.php?t=32039#p202158 Invalid Event Objects
function Event.dispatch(event)
if type(event) ~= 'table' then
error('missing event table')
end
--get the registered handlers from name, input_name, or nth_tick in that priority.
local registry
if event.name and Event.registry[event.name] then
registry = Event.registry[event.name]
elseif event.input_name and Event.registry[event.input_name] then
registry = Event.registry[event.input_name]
elseif event.nth_tick then
registry = Event.registry[-event.nth_tick]
end
if registry then
--add the tick if it is not present, this only affects calling Event.dispatch manually
--doing the check up here as it will faster than checking every iteration for a constant value
event.tick = event.tick or (game and game.tick) or 0
event.define_name = event_names[event.name or '']
event.options = event.options or {}
-- Some events are just stupid and need more help
if stupid_events[event.name] then
event.created_entity = event.created_entity or event.entity or event.destination
end
for _, registered in ipairs(registry) do
event.options = setmetatable(event.options, { __index = registered.options })
-- Check for userdata and stop processing this and further handlers if not valid
-- This is the same behavior as factorio events.
-- This is done inside the loop as other events can modify the event.
if not event.options.skip_valid then
for _, val in pairs(event) do
if type(val) == 'table' and val.__self and not val.valid then
return
end
end
end
-- Dispatch the event, if the event return Event.stop_processing don't process any more events
if dispatch_event(event, registered) == Event.stop_processing then
return
end
-- Force a crc check if option is enabled. This is a debug option and will hamper performance if enabled
if game and event.options.force_crc then
log('CRC check called for event [' .. event.name .. ']')
game.force_crc()
end
end
end
end
function Event.register_player(bool)
require('__stdlib__/stdlib/event/player').register_events(bool)
return Event
end
function Event.register_force(bool)
require('__stdlib__/stdlib/event/force').register_events(bool)
return Event
end
function Event.register_surface(bool)
require('__stdlib__/stdlib/event/surface').register_events(bool)
return Event
end
--- Retrieve or Generate an event_name and store it in Event.custom_events
-- @tparam string event_name the custom name for your event.
-- @treturn int the id associated with the event.
-- @usage
-- Event.register(Event.generate_event_name("my_custom_event"), handler)
function Event.generate_event_name(event_name)
assert(Type.String(event_name), 'event_name must be a string.')
local id
if Type.Number(Event.custom_events[event_name]) then
id = Event.custom_events[event_name]
else
id = Event.script.generate_event_name()
Event.custom_events[event_name] = id
end
return id
end
function Event.set_event_name(event_name, id)
assert(Type.String(event_name), 'event_name must be a string')
assert(Type.Number(id))
Event.custom_events[event_name] = id
return Event.custom_events[event_name]
end
function Event.get_event_name(event_name)
assert(Type.String(event_name), 'event_name must be a string')
return Event.custom_events[event_name]
end
---@todo complete stub
function Event.raise_event(...)
script.raise_event(...)
end
--- Get event handler.
function Event.get_event_handler(event_id)
assert(valid_id(event_id), 'event_id is invalid')
return {
script = bootstrap_events[event_id] or (valid_event_id(event_id) and Event.script.get_event_handler(event_id)),
handlers = Event.registry[event_id]
}
end
--- Set protected mode.
function Event.set_protected_mode(bool)
Event.options.protected_mode = bool and true or false
return Event
end
--- Set debug mode default for Event module.
function Event.set_debug_mode(bool)
Event.debug_mode = bool and true or false
return Event
end
--- Set default options for the event module.
function Event.set_option(option, bool)
Event.options[option] = bool and true or false
return Event
end
Event.dump_data = require('__stdlib__/stdlib/event/modules/dump_event_data')(Event, valid_event_id, id_to_name)
return Event

View File

@@ -0,0 +1,125 @@
--- Force global creation.
-- <p>All new forces will be added to the `global.forces` table.
-- <p>This modules events should be registered after any other Init functions but before any scripts needing `global.players`.
-- <p>This modules can register the following events: `on_force_created`, and `on_forces_merging`.
-- @module Event.Force
-- @usage
-- local Force = require('__stdlib__/stdlib/event/force').register_events()
-- -- inside your Init event Force.init() -- to properly handle any existing forces
local Event = require('__stdlib__/stdlib/event/event')
local Force = {
__class = 'Force',
__index = require('__stdlib__/stdlib/core'),
_new_force_data = {}
}
setmetatable(Force, Force)
local inspect = _ENV.inspect
local Game = require('__stdlib__/stdlib/game')
local table = require('__stdlib__/stdlib/utils/table')
local merge_additional_data = require('__stdlib__/stdlib/event/modules/merge_data')
local assert, type = assert, type
-- return new default force object
local function new(force_name)
local fdata = {
index = force_name,
name = force_name
}
merge_additional_data(Force._new_force_data, fdata)
return fdata
end
function Force.additional_data(...)
for _, func_or_table in pairs { ... } do
local var_type = type(func_or_table)
assert(var_type == 'table' or var_type == 'function', 'Must be table or function')
Force._new_force_data[#Force._new_force_data + 1] = func_or_table
end
return Force
end
--- Get `game.forces[name]` & `global.forces[name]`, or create `global.forces[name]` if it doesn't exist.
-- @tparam string|LuaForce force the force to get data for
-- @treturn LuaForce the force instance
-- @treturn table the force's global data
-- @usage
-- local Force = require('__stdlib__/stdlib/event/force')
-- local force_name, force_data = Force.get("player")
-- local force_name, force_data = Force.get(game.forces["player"])
-- -- Returns data for the force named "player" from either a string or LuaForce object
function Force.get(force)
force = Game.get_force(force)
assert(force, 'force is missing')
return game.forces[force.name], global.forces and global.forces[force.name] or Force.init(force.name)
end
--- Merge a copy of the passed data to all forces in `global.forces`.
-- @tparam table data a table containing variables to merge
-- @usage
-- local data = {a = "abc", b = "def"}
-- Force.add_data_all(data)
function Force.add_data_all(data)
table.each(
global.forces,
function(v)
table.merge(v, table.deepcopy(data))
end
)
end
--- Init or re-init a force or forces.
-- Passing a `nil` event will iterate all existing forces.
-- @tparam[opt] string|table event table or a string containing force name
-- @tparam[opt=false] boolean overwrite the force data
function Force.init(event, overwrite)
global.forces = global.forces or {}
local force = Game.get_force(event)
if force then
if not global.forces[force.name] or (global.forces[force.name] and overwrite) then
global.forces[force.name] = new(force.name)
return global.forces[force.name]
end
else
for name in pairs(game.forces) do
if not global.forces[name] or (global.forces[name] and overwrite) then
global.forces[name] = new(name)
end
end
end
return Force
end
function Force.dump_data()
game.write_file(Force.get_file_path('Force/force_data.lua'), 'return ' .. inspect(Force._new_force_data, { longkeys = true, arraykeys = true }))
game.write_file(Force.get_file_path('Force/global.lua'), 'return ' .. inspect(global.forces or nil, { longkeys = true, arraykeys = true }))
end
--- When forces are merged, just remove the original forces data
function Force.merged(event)
global.forces[event.source_name] = nil
end
function Force.register_init()
Event.register(Event.core_events.init, Force.init)
return Force
end
--- Register Events
function Force.register_events(do_on_init)
Event.register(defines.events.on_force_created, Force.init)
Event.register(defines.events.on_forces_merged, Force.merged)
if do_on_init then
Force.register_init()
end
return Force
end
return Force

View File

@@ -0,0 +1,78 @@
--- Makes monolithic Factorio GUI events more manageable.
-- @module Event.Gui
-- @usage local Gui = require('__stdlib__/stdlib/event/gui')
local Event = require('__stdlib__/stdlib/event/event')
local Gui = {
__class = 'Gui',
__index = require('__stdlib__/stdlib/core')
}
setmetatable(Gui, Gui)
--- Registers a function for a given gui element name or pattern when the element is clicked.
-- @tparam string gui_element_pattern the name or string regular expression to match the gui element
-- @tparam function handler the function to call when gui element is clicked
-- @return (<span class="types">@{Gui}</span>)
function Gui.on_click(gui_element_pattern, handler)
Event.register(defines.events.on_gui_click, handler, Event.Filters.gui, gui_element_pattern)
return Gui
end
--- Registers a function for a given GUI element name or pattern when the element checked state changes.
-- @tparam string gui_element_pattern the name or string regular expression to match the GUI element
-- @tparam function handler the function to call when GUI element checked state changes
-- @return (<span class="types">@{Gui}</span>)
function Gui.on_checked_state_changed(gui_element_pattern, handler)
Event.register(defines.events.on_gui_checked_state_changed, handler, Event.Filters.gui, gui_element_pattern)
return Gui
end
--- Registers a function for a given GUI element name or pattern when the element text changes.
-- @tparam string gui_element_pattern the name or string regular expression to match the GUI element
-- @tparam function handler the function to call when GUI element text changes
-- @return (<span class="types">@{Gui}</span>)
function Gui.on_text_changed(gui_element_pattern, handler)
Event.register(defines.events.on_gui_text_changed, handler, Event.Filters.gui, gui_element_pattern)
return Gui
end
--- Registers a function for a given GUI element name or pattern when the element selection changes.
-- @tparam string gui_element_pattern the name or string regular expression to match the GUI element
-- @tparam function handler the function to call when GUI element selection changes
-- @return (<span class="types">@{Gui}</span>)
function Gui.on_elem_changed(gui_element_pattern, handler)
Event.register(defines.events.on_gui_elem_changed, handler, Event.Filters.gui, gui_element_pattern)
return Gui
end
--- Registers a function for a given GUI element name or pattern when the element state changes (drop down).
-- @tparam string gui_element_pattern the name or string regular expression to match the GUI element
-- @tparam function handler the function to call when GUI element state changes
-- @return (<span class="types">@{Gui}</span>)
function Gui.on_selection_state_changed(gui_element_pattern, handler)
Event.register(defines.events.on_gui_selection_state_changed, handler, Event.Filters.gui, gui_element_pattern)
return Gui
end
--- Registers a function for a given GUI element name or pattern when the element value changes (slider).
-- @tparam string gui_element_pattern the name or string regular expression to match the GUI element
-- @tparam function handler the function to call when GUI element state changes
-- @return (<span class="types">@{Gui}</span>)
function Gui.on_value_changed(gui_element_pattern, handler)
Event.register(defines.events.on_gui_value_changed, handler, Event.Filters.gui, gui_element_pattern)
return Gui
end
--- Registers a function for a given GUI element name or pattern when the element is confirmed.
-- @tparam string gui_element_pattern the name or string regular expression to match the GUI element
-- @tparam function handler the function to call when GUI element state changes
-- @return (<span class="types">@{Gui}</span>)
function Gui.on_confirmed(gui_element_pattern, handler)
Event.register(defines.events.on_gui_confirmed, handler, Event.Filters.gui, gui_element_pattern)
return Gui
end
Event.Gui = Gui
return Gui

View File

@@ -0,0 +1,61 @@
local inspect = _ENV.inspect
local function setup_event_data(Event, valid_event_id, id_to_name)
local function get_registered_counts(reg_type)
local core, nth, on_events = 0, 0, 0
local events = {}
for id, registry in pairs(Event.registry) do
if tonumber(id) then
if id < 0 then
nth = nth + #registry
else
on_events = on_events + #registry
end
else
if Event.core_events[id] then
core = core + #registry
else
on_events = on_events + #registry
end
end
local name = id_to_name(id)
events[name] = (events[name] or 0) + #registry
end
local all = {
core = core,
events = events,
nth = nth,
on_events = on_events,
total = on_events + nth + core
}
return reg_type and all[reg_type] or all
end
local function dump_data()
local event_data = {
count_data = get_registered_counts(),
event_order = script.get_event_order(),
custom_events = Event.custom_events,
registry = Event.registry,
options = {
protected_mode = Event.options.protected_mode,
force_crc = Event.options.force_crc,
inspect_event = Event.options.inspect_event,
skip_valid = Event.options.skip_valid
}
}
local registry, factorio_events = {}, {}
for event, data in pairs(Event.registry) do
registry['[' .. event .. '] ' .. id_to_name(event)] = data
if valid_event_id(event) then
factorio_events['[' .. event .. '] ' .. id_to_name(event)] = Event.script.get_event_handler(event)
end
end
game.write_file(Event.get_file_path('Event/Event.lua'), 'return ' .. inspect(event_data))
game.write_file(Event.get_file_path('Event/Event.registry.lua'), 'return ' .. inspect(registry, { longkeys = true, arraykeys = true }))
game.write_file(Event.get_file_path('Event/Factorio.registry.lua'), 'return ' .. inspect(factorio_events, { longkeys = true, arraykeys = true }))
end
return dump_data
end
return setup_event_data

View File

@@ -0,0 +1,45 @@
--- Event Filters
-- Predefined event filter functions
-- @module Event.Filters
local Filters = {
entity = {},
player = {},
}
function Filters.on_key(event_key, pattern)
return function(event)
local key = event and event[event_key]
return key and key:match(pattern)
end
end
function Filters.entity.name(event, pattern)
local entity = event and (event.created_entity or event.entity)
return entity.name:match(pattern)
end
function Filters.entity.type(event, pattern)
local entity = event and (event.created_entity or event.entity)
return entity.type:match(pattern)
end
function Filters.player.cursor_stack(event, pattern)
local player = game.get_player(event.player_index)
local stack = player.cursor_stack
return stack and stack.valid_for_read and stack.name:match(pattern)
end
function Filters.gui(event, pattern)
if event.element and event.element.valid then
local match_str = event.element.name:match(pattern)
if match_str then
event.match = match_str
event.state = event.name == defines.events.on_gui_checked_state_changed and event.element.state or nil
event.text = event.name == defines.events.on_gui_text_changed and event.element.text or nil
return match_str
end
end
end
return Filters

View File

@@ -0,0 +1,21 @@
local table = require('__stdlib__/stdlib/utils/table')
local function merge_additional_data(additional_data_array, data)
for _, new_data in pairs(additional_data_array) do
if type(new_data) == 'table' then
table.merge(data, table.deepcopy(new_data))
elseif type(new_data) == 'function' then
local new_data_func_result = new_data(data.index)
if type(new_data_func_result) == 'table' then
table.merge(data, new_data_func_result)
else
error('additional data function did not return a table')
end
else
error('additional data present but is not a function or table')
end
end
return data
end
return merge_additional_data

View File

@@ -0,0 +1,143 @@
--- Player global creation.
-- This module adds player helper functions, it does not automatically register events unless Player.register_events() is called
-- @module Event.Player
-- @usage
-- local Player = require('__stdlib__/stdlib/event/player').register_events()
-- -- The fist time this is required it will register player creation events
local Event = require('__stdlib__/stdlib/event/event')
local Player = {
__class = 'Player',
__index = require('__stdlib__/stdlib/core'),
_new_player_data = {}
}
setmetatable(Player, Player)
local Game = require('__stdlib__/stdlib/game')
local table = require('__stdlib__/stdlib/utils/table')
local merge_additional_data = require('__stdlib__/stdlib/event/modules/merge_data')
local assert, type = assert, type
local inspect = _ENV.inspect
-- Return new default player object consiting of index, name, force
local function new(player_index)
local pdata = {
index = player_index,
name = game.players[player_index].name,
force = game.players[player_index].force.name
}
merge_additional_data(Player._new_player_data, pdata)
return pdata
end
function Player.additional_data(...)
for _, func_or_table in pairs { ... } do
local var_type = type(func_or_table)
assert(var_type == 'table' or var_type == 'function', 'Must be table or function')
Player._new_player_data[#Player._new_player_data + 1] = func_or_table
end
return Player
end
--- Get `game.players[index]` & `global.players[index]`, or create `global.players[index]` if it doesn't exist.
-- @tparam number|string|LuaPlayer player the player index to get data for
-- @treturn LuaPlayer the player instance
-- @treturn table the player's global data
-- @usage
-- local Player = require('__stdlib__/stdlib/event/player')
-- local player, player_data = Player.get(event.player_index)
function Player.get(player)
player = Game.get_player(player)
return player, global.players and global.players[player.index] or Player.init(player.index)
end
--- Get the players saved data table. Creates it if it doesn't exsist.
-- @tparam number index The player index to get data for
-- @treturn table the player's global data
function Player.pdata(index)
return global.players and global.players[index] or Player.init(index)
end
--- Merge a copy of the passed data to all players in `global.players`.
-- @tparam table data a table containing variables to merge
-- @usage local data = {a = 'abc', b = 'def'}
-- Player.add_data_all(data)
function Player.add_data_all(data)
local pdata = global.players
table.each(
pdata,
function(v)
table.merge(v, table.deepcopy(data))
end
)
end
--- Remove data for a player when they are deleted.
-- @tparam table event event table containing the `player_index`
function Player.remove(event)
global.players[event.player_index] = nil
end
--- Init or re-init a player or players.
-- Passing a `nil` event will iterate all existing players.
-- @tparam[opt] number|table|string|LuaPlayer event
-- @tparam[opt=false] boolean overwrite the player data
function Player.init(event, overwrite)
-- Create the global.players table if it doesn't exisit
global.players = global.players or {}
--get a valid player object or nil
local player = Game.get_player(event)
if player then --If player is not nil then we are working with a valid player.
if not global.players[player.index] or (global.players[player.index] and overwrite) then
global.players[player.index] = new(player.index)
return global.players[player.index]
end
else --Check all players
for index in pairs(game.players) do
if not global.players[index] or (global.players[index] and overwrite) then
global.players[index] = new(index)
end
end
end
if global._print_queue then
table.each(
global._print_queue,
function(msg)
game.print(tostring(msg))
end
)
global._print_queue = nil
end
return Player
end
function Player.update_force(event)
local player, pdata = Player.get(event.player_index)
pdata.force = player.force.name
end
function Player.dump_data()
game.write_file(Player.get_file_path('Player/player_data.lua'), 'return ' .. inspect(Player._new_player_data, { longkeys = true, arraykeys = true }))
game.write_file(Player.get_file_path('Player/global.lua'), 'return ' .. inspect(global.players or nil, { longkeys = true, arraykeys = true }))
end
function Player.register_init()
Event.register(Event.core_events.init, Player.init)
return Player
end
function Player.register_events(do_on_init)
Event.register(defines.events.on_player_created, Player.init)
Event.register(defines.events.on_player_changed_force, Player.update_force)
Event.register(defines.events.on_player_removed, Player.remove)
if do_on_init then
Player.register_init()
end
return Player
end
return Player

View File

@@ -0,0 +1,105 @@
--- Surface global creation.
-- <p>All surfaces will be added to the `global.surfaces` table.
-- <p>This modules events should be registered after any other Init functions but before any scripts needing `global.surfaces`.
-- <p>This modules can register the following events:
-- @module Event.Surface
-- @usage
-- local surface = require('__stdlib__/stdlib/event/surface').register_events()
local Event = require('__stdlib__/stdlib/event/event')
local Surface = {
__class = 'Surface',
_new_surface_data = {}
}
setmetatable(Surface, require('__stdlib__/stdlib/core'))
local inspect = _ENV.inspect
local merge_additional_data = require('__stdlib__/stdlib/event/modules/merge_data')
local function new(index)
local surface = game.surfaces[index]
local sdata = {
index = surface.index,
name = surface.name,
}
merge_additional_data(Surface._new_surface_data, sdata)
return sdata
end
function Surface.additional_data(...)
for _, func_or_table in pairs { ... } do
local typeof = type(func_or_table)
assert(typeof == 'table' or typeof == 'string', 'Must be table or function')
Surface._new_surface_data[#Surface._new_surface_data + 1] = func_or_table
end
return Surface
end
--- Remove data for a surface when it is deleted.
-- @tparam table event event table containing the surface index
function Surface.remove(event)
global.surfaces[event.surface_index] = nil
end
function Surface.rename(event)
global.surfaces[event.surface_index].name = event.new_name
end
function Surface.import(event)
new(event.surface_index)
end
-- function Surface.cleared(event)
-- end
--- Init or re-init the surfaces.
-- Passing a `nil` event will iterate all existing surfaces.
-- @tparam[opt] number|table|string|LuaSurface event
-- @tparam[opt=false] boolean overwrite the surface data
function Surface.init(event, overwrite)
-- Create the global.surfaces table if it doesn't exisit
global.surfaces = global.surfaces or {}
--get a valid surface object or nil
local surface = game.surfaces[event.surface_index]
if surface then
if not global.surfaces[surface.index] or (global.surfaces[surface.index] and overwrite) then
global.surfaces[surface.index] = new(surface.index)
return global.surfaces[surface.index]
end
else --Check all surfaces
for index in pairs(game.surfaces) do
if not global.surfaces[index] or (global.surfaces[index] and overwrite) then
global.surfaces[index] = new(index)
end
end
end
return Surface
end
function Surface.dump_data()
game.write_file(Surface.get_file_path('Surface/surface_data.lua'), inspect(Surface._new_surface_data, { longkeys = true, arraykeys = true }))
game.write_file(Surface.get_file_path('Surface/global.lua'), inspect(global.surfaces or nil, { longkeys = true, arraykeys = true }))
end
function Surface.register_init()
Event.register(Event.core_events.init, Surface.init)
return Surface
end
function Surface.register_events(do_on_init)
Event.register(defines.events.on_surface_created, Surface.init)
Event.register(defines.events.on_surface_deleted, Surface.remove)
Event.register(defines.events.on_surface_imported, Surface.import)
Event.register(defines.events.on_surface_renamed, Surface.rename)
--Event.register(defines.events.on_surface_cleared, Surface.func)
if do_on_init then
Surface.register_init()
end
return Surface
end
return Surface

View File

@@ -0,0 +1,214 @@
--- Tools for working with trains.
-- When this module is loaded into a mod, it automatically registers a number of new events in order to keep track of
-- the trains as their locomotives and wagons are moved around.
-- <p>To handle the events, you should use the @{Event} module.
-- @module Event.Trains
local Trains = {
__class = 'Trains',
__index = require('__stdlib__/stdlib/core')
}
setmetatable(Trains, Trains)
local Event = require('__stdlib__/stdlib/event/event')
local Surface = require('__stdlib__/stdlib/area/surface')
local Entity = require('__stdlib__/stdlib/entity/entity')
local table = require('__stdlib__/stdlib/utils/table')
--- This event fires when a train's ID changes.
-- <p>The train ID is a property of the main locomotive,
-- which means that when locomotives are attached or detached from their wagons or from other locomotives, the ID of the train changes.
-- <p>For example: A train with a front and rear locomotives will get its ID
-- from the front locomotive. If the front locomotive gets disconnected, the rear locomotive becomes the main one and the train's ID changes.
-- @event on_train_id_changed
-- @tparam uint old_id the ID of the train before the change
-- @tparam uint new_id the ID of the train after the change
-- @usage
---- Event.register(Trains.on_train_id_changed, my_handler)
Trains.on_train_id_changed = Event.generate_event_name()
--- Given a @{criteria|search criteria}, search for trains that match the criteria.
-- If ***criteria.surface*** is not supplied, this function searches through all existing surfaces.
-- If ***criteria.force*** is not supplied, this function searches through all existing forces.
-- If ***criteria.state*** is not supplied, this function gets trains in any @{defines.train_state|state}.
-- @tparam criteria criteria a table used to search for trains
-- @return (<span class="types">{@{train_details},...}</span>) an array of train IDs and LuaTrain instances
-- @usage
-- Trains.find_filtered({ surface = "nauvis", state = defines.train_state.wait_station })
function Trains.find_filtered(criteria)
criteria = criteria or {}
local surface_list = Surface.lookup(criteria.surface)
if criteria.surface == nil then
surface_list = game.surfaces
end
local results = {}
for _, surface in pairs(surface_list) do
local trains = surface.get_trains(criteria.force)
for _, train in pairs(trains) do
table.insert(results, train)
end
end
-- Apply state filters
if criteria.state then
results =
table.filter(
results,
function(train)
return train.state == criteria.state
end
)
end
-- Lastly, look up the train ids
results =
table.map(
results,
function(train)
return { train = train, id = Trains.get_main_locomotive(train).unit_number }
end
)
return results
end
---
-- This table should be passed into @{find_filtered} to find trains that match the criteria.
-- @tfield[opt] ?|nil|string|{string,...}|LuaSurface|{LuaSurface,...} surface the surfaces to look up for the trains
-- @tfield[opt] ?|nil|string|LuaForce force the force of the trains to search
-- @tfield[opt] ?|nil|defines.train_state state the state of the trains to search
-- @table criteria
---
-- @{find_filtered} returns an array with one or more of ***this*** table based on the @{criteria|search criteria}.
-- @tfield LuaTrain train an instance of the train
-- @tfield uint id the ID of the train
-- @table train_details
--- Find the ID of a LuaTrain instance.
-- @tparam LuaTrain train
-- @treturn uint the ID of the train
function Trains.get_train_id(train)
local loco = Trains.get_main_locomotive(train)
return loco and loco.unit_number
end
--- Event fired when some change happens to a locomotive.
-- @lfunction
function Trains._on_locomotive_changed()
-- For all the known trains
local renames = {}
for id, train in pairs(global._train_registry) do
-- Check if their known ID is the same as the LuaTrain's dervied id
local derived_id = Trains.get_train_id(train)
-- If it's not
if (id ~= derived_id) then
-- Capture the rename
table.insert(renames, { old_id = id, new_id = derived_id, train = train })
end
end
-- Go over the captured renaming operations
for _, renaming in pairs(renames) do
-- Rename it in the registry
-- and dispatch a renamed event
global._train_registry[renaming.new_id] = renaming.train
global._train_registry[renaming.old_id] = nil
local event_data = {
old_id = renaming.old_id,
new_id = renaming.new_id,
name = Trains.on_train_id_changed
}
Event.dispatch(event_data)
end
end
--- Get the main locomotive of a train.
-- @tparam LuaTrain train
-- @treturn LuaEntity the main locomotive
function Trains.get_main_locomotive(train)
if train and train.valid and train.locomotives and (#train.locomotives.front_movers > 0 or #train.locomotives.back_movers > 0) then
return train.locomotives.front_movers and train.locomotives.front_movers[1] or train.locomotives.back_movers[1]
end
end
--- Creates an entity from a train that is compatible with the @{Entity.Entity} module.
-- @tparam LuaTrain train
-- @return (<span class="types">@{train_entity}</span>)
function Trains.to_entity(train)
local name = 'train-' .. Trains.get_train_id(train)
return {
name = name,
valid = train.valid,
equals = function(entity)
return name == entity.name
end
}
end
------
-- @{to_entity} returns ***this*** table.
-- @tfield string name the name of the train entity with the train ID as its suffix
-- @tfield boolean valid whether or not if the train is in a valid state in the game
-- @tfield function equals &mdash; *function(entity)* &mdash; a function to check if another entity is equal to the train that ***this*** table represents
-- @table train_entity
--- Associates the user data to a train.
-- This is a helper around @{Entity.Entity.set_data}.
-- <p>The user data will be stored in the global object and it will persist between loads.
--> The user data will be removed from a train when the train becomes invalid.
-- @tparam LuaTrain train the train to set the user data for
-- @tparam ?|nil|Mixed data the user data to set, or nil to delete the user data associated with the train
-- @treturn ?|nil|Mixed the previous user data or nil if the train had no previous user data
function Trains.set_data(train, data)
return Entity.set_data(Trains.to_entity(train), data)
end
--- Gets the user data that is associated with a train.
-- This is a helper around @{Entity.Entity.get_data}.
-- <p>The user data is stored in the global object and it persists between loads.
--> The user data will be removed from a train when the train becomes invalid.
-- @tparam LuaTrain train the train to look up user data for
-- @treturn ?|nil|Mixed the user data, or nil if no user data exists for the train
function Trains.get_data(train)
return Entity.get_data(Trains.to_entity(train))
end
-- Creates a registry of known trains.
-- @return table a mapping of train id to LuaTrain object
function Trains.create_train_registry()
global._train_registry = global._train_registry or {}
local all_trains = Trains.find_filtered()
for _, trainInfo in pairs(all_trains) do
global._train_registry[tonumber(trainInfo.id)] = trainInfo.train
end
return global._train_registry
end
function Trains.on_train_created(event)
local train_id = Trains.get_train_id(event.train)
global._train_registry[train_id] = event.train
end
--- This needs to be called to register events for this module
-- @treturn Trains
function Trains.register_events()
-- When a locomotive is removed ...
local train_remove_events = { defines.events.on_entity_died, defines.events.on_pre_player_mined_item, defines.events.on_robot_pre_mined }
Event.register(train_remove_events, Trains._on_locomotive_changed, Event.Filters.entity.type, 'locomotive')
-- When a locomotive is added ...
Event.register(defines.events.on_train_created, Trains.on_train_created)
-- When the mod is initialized the first time
Event.register(Event.core_events.init_and_config, Trains.create_train_registry)
return Trains
end
return Trains

View File

@@ -0,0 +1,133 @@
--- The game module.
-- @module Game
-- @usage local Game = require('__stdlib__/stdlib/game')
local Game = {
__class = 'Game',
__index = require('__stdlib__/stdlib/core')
}
setmetatable(Game, Game)
local inspect = _ENV.inspect
--- Return a valid player object from event, index, string, or userdata
-- @tparam string|number|LuaPlayer|event mixed
-- @treturn LuaPlayer a valid player or nil
function Game.get_player(mixed)
if type(mixed) == 'table' then
if mixed.player_index then
return game.get_player(mixed.player_index)
elseif mixed.__self then
return mixed.valid and mixed
end
elseif mixed then
return game.get_player(mixed)
end
end
--- Return a valid force object from event, string, or userdata
-- @tparam string|LuaForce|event mixed
-- @treturn LuaForce a valid force or nil
function Game.get_force(mixed)
if type(mixed) == 'table' then
if mixed.__self then
return mixed and mixed.valid and mixed
elseif mixed.force then
return Game.get_force(mixed.force)
end
elseif type(mixed) == 'string' then
local force = game.forces[mixed]
return (force and force.valid) and force
end
end
function Game.get_surface(mixed)
if type(mixed) == 'table' then
if mixed.__self then
return mixed.valid and mixed
elseif mixed.surface then
return Game.get_surface(mixed.surface)
end
elseif mixed then
local surface = game.surfaces[mixed]
return surface and surface.valid and surface
end
end
--- Messages all players currently connected to the game.
--> Offline players are not counted as having received the message.
-- If no players exist msg is stored in the `global._print_queue` table.
-- @tparam string msg the message to send to players
-- @tparam[opt] ?|nil|boolean condition the condition to be true for a player to be messaged
-- @treturn uint the number of players who received the message.
function Game.print_all(msg, condition)
local num = 0
if #game.players > 0 then
for _, player in pairs(game.players) do
if condition == nil or select(2, pcall(condition, player)) then
player.print(msg)
num = num + 1
end
end
return num
else
global._print_queue = global._print_queue or {}
global._print_queue[#global._print_queue + 1] = msg
end
end
--- Gets or sets data in the global variable.
-- @tparam string sub_table the name of the table to use to store data.
-- @tparam[opt] mixed index an optional index to use for the sub_table
-- @tparam mixed key the key to store the data in
-- @tparam[opt] boolean set store the contents of value, when true return previously stored data
-- @tparam[opt] mixed value when set is true set key to this value, if not set and key is empty store this
-- @treturn mixed the chunk value stored at the key or the previous value
function Game.get_or_set_data(sub_table, index, key, set, value)
assert(type(sub_table) == 'string', 'sub_table must be a string')
global[sub_table] = global[sub_table] or {}
local this
if index then
global[sub_table][index] = global[sub_table][index] or {}
this = global[sub_table][index]
else
this = global[sub_table]
end
local previous
if set then
previous = this[key]
this[key] = value
return previous
elseif not this[key] and value then
this[key] = value
return this[key]
end
return this[key]
end
function Game.write_mods()
game.write_file('Mods.lua', 'return ' .. inspect(game.active_mods))
end
function Game.write_statistics()
local pre = 'Statistics/' .. game.tick .. '/'
for _, force in pairs(game.forces) do
local folder = pre .. force.name .. '/'
for _, count_type in pairs { 'input_counts', 'output_counts' } do
game.write_file(folder .. 'pollution-' .. count_type .. '.json', game.table_to_json(game.pollution_statistics[count_type]))
game.write_file(folder .. 'item-' .. count_type .. '.json', game.table_to_json(force.item_production_statistics[count_type]))
game.write_file(folder .. 'fluid-' .. count_type .. '.json', game.table_to_json(force.fluid_production_statistics[count_type]))
game.write_file(folder .. 'kill-' .. count_type .. '.json', game.table_to_json(force.kill_count_statistics[count_type]))
game.write_file(folder .. 'build-' .. count_type .. '.json', game.table_to_json(force.entity_build_count_statistics[count_type]))
end
end
end
function Game.write_surfaces()
game.remove_path('surfaces')
for _, surface in pairs(game.surfaces) do
game.write_file('surfaces/' .. (surface.name or surface.index) .. '.lua', 'return ' .. inspect(surface.map_gen_settings))
end
end
return Game

View File

@@ -0,0 +1,200 @@
--- For working with mod configurations.
-- @module Misc.Config
-- @usage require('__stdlib__/stdlib/config/config')
---
-- @tfield function new
-- @tfield function get
-- @tfield function set
-- @tfield function delete
-- @tfield function is_set
-- @table Config
local M = {
__class = 'Config',
__index = require('__stdlib__/stdlib/core')
}
setmetatable(M, M)
local table = require('__stdlib__/stdlib/utils/table')
local string = require('__stdlib__/stdlib/utils/string')
-----------------------------------------------------------------------
--Setup repeated code for use in sub functions here
-----------------------------------------------------------------------
local reservedCharacters = [[`~!@#$%^&*+=|;:/\\\'",?()[]{}<>]]
local testReservedCharacters = function(path)
local reserved = reservedCharacters
for c in reserved:gmatch('.') do
if path:find(c, 1, true) then
return c
end
end
return nil
end
--- Creates a new Config object to ease the management of a config table.
-- @tparam table config_table the config table to manage
-- @treturn Config the Config object to manage the config table
--
-- @usage --[Use a global table for config that persists across game save/loads]
-- CONFIG = Config.new(global.testtable)
--
-- @usage --[You can also create a temporary scratch pad config]
-- CONFIG = Config.new({}) -- Temporary scratch pad
--
-- @usage --[Setting data in Config]
-- CONFIG = Config.new(global.testtable)
-- CONFIG.set("your.path.here", "myvalue")
--
-- @usage --[Getting data out of Config]
-- CONFIG = Config.new(global.testtable)
-- my_data = CONFIG.get("your.path.here")
--
-- @usage --[Getting data out of Config with a default to use if path is not found in Config]
-- CONFIG = Config.new(global.testtable)
-- my_data = CONFIG.get("your.path.here", "Your Default here")
--
-- @usage --[Deleting a path from Config]
-- CONFIG = Config.new(global.testtable)
-- CONFIG.delete("your.path.here")
--
-- @usage --[Checking if a path exists in Config]
-- CONFIG = Config.new(global.testtable)
-- CONFIG.is_set("your.path.here")
function M.new(config_table)
if not config_table then
error('config_table is a required parameter.', 2)
elseif type(config_table) ~= 'table' then
error('config_table must be a table. Was given [' .. type(config_table) .. ']', 2)
elseif type(config_table.get) == 'function' then
error("Config can't manage another Config object", 2)
end
-----------------------------------------------------------------------
--Setup the Config object
-----------------------------------------------------------------------
local Config = {}
--- Get a stored config value.
-- @tparam string path the variable to retrieve
-- @tparam[opt] Mixed default value to be used if path is nil
-- @treturn Mixed value at path or nil if not found and no default given
function Config.get(path, default)
if type(path) ~= 'string' or path == '' then
error('path is invalid', 2)
end
local config = config_table
local c = testReservedCharacters(path)
if c ~= nil then
error("path '" .. path .. "' contains the reserved character '" .. c .. "'", 2)
end
local pathParts = string.split(path, '.')
local part = config
local value = nil
for key = 1, #pathParts, 1 do
local partKey = pathParts[key]
if (type(part) ~= 'table') then
value = nil
break
end
value = part[partKey]
part = part[partKey]
end
if (type(value) == 'table') then
--Force break references.
return table.deepcopy(value)
elseif (value ~= nil) then
return value
else
return default
end
end
--- Set a stored config value.
-- @tparam string path the config path to set
-- @tparam ?|nil|Mixed data the value to set the path to. If *nil*, it behaves identical to @{delete|Config.delete()}
-- @treturn uint 0 on failure or the number of affected paths on success
function Config.set(path, data)
if type(path) ~= 'string' or path == '' then
error('path is invalid', 2)
end
local config = config_table
local c = testReservedCharacters(path)
if c ~= nil then
error("path contains the reserved character '" .. c .. "'", 2)
end
local pathParts = string.split(path, '.')
local part = config
for key = 1, #pathParts - 1, 1 do
local partKey = pathParts[key]
if (type(part[partKey]) ~= 'table') then
part[partKey] = {}
end
part = part[partKey]
end
part[pathParts[#pathParts]] = data
return 1
end
--- Delete a stored config value.
-- @tparam string path the config path to delete
-- @treturn uint 0 on failure or the number of affected paths on success
function Config.delete(path)
if type(path) ~= 'string' or string == '' then
error('path is invalid', 2)
end
local config = config_table
local c = testReservedCharacters(path)
if c ~= nil then
error("path contains the reserved character '" .. c .. "'", 2)
end
local pathParts = string.split(path, '.')
local part = config
for key = 1, #pathParts - 1, 1 do
local partKey = pathParts[key]
if (type(part[partKey]) ~= 'table') then
return 0
end
part = part[partKey]
end
if part[pathParts[#pathParts]] == nil then
return 0
else
part[pathParts[#pathParts]] = nil
return 1
end
end
--- Test the existence of a stored config value.
-- @tparam string path the config path to test
-- @treturn boolean true if the value exists, false otherwise
function Config.is_set(path)
if type(path) ~= 'string' or path == '' then
error('path is invalid', 2)
end
return Config.get(path) ~= nil
end
return Config
end
return M

View File

@@ -0,0 +1,153 @@
--- For logging debug information to files.
-- @module Misc.Logger
-- @usage
-- local Logger = require('__stdlib__/stdlib/misc/logger')
-- -- or to create a new logger directly:
-- local Log = require('__stdlib__/stdlib/misc/logger').new()
-- -- log files are saved to script-output/modname/log.log by default
local Logger = {
__class = 'Logger',
_loggers = {},
__call = function(self, ...)
return self.get(...)
end,
__index = require('__stdlib__/stdlib/core')
}
setmetatable(Logger, Logger)
local table = require('__stdlib__/stdlib/utils/table')
-- Set on the individual log object, either logs a message or writes immediatly if nil.
local _Log_mt = {
__call = function(self, msg)
if msg then
return self, self.log(msg)
else
return self, self.write()
end
end
}
local format = string.format
--- Get a saved log or create a new one if there is no saved log.
function Logger.get(...)
local log_name = (...) or 'log'
return Logger._loggers[log_name] or Logger.new(...)
end
--- Creates a new logger object.
-- In debug mode, the logger writes to file immediately, otherwise the logger buffers the lines.
-- <p>The logger flushes the logged messages every 60 seconds since the last message.
-- <p>A table of @{options} may be specified when creating a logger.
-- @usage
--Log = Logger.new()
--Log("this msg will be logged in /script-output/YourModName/log.log!")
-- -- Immediately Write everything buffered in the log file
-- Log()
--
-- @usage
--Log = Logger.new('test', true)
--Log("this msg will be logged and written immediately in /script-output/YourModName/test.log!")
--
-- @usage
--Log = Logger.new('cool_mod_name', 'test', true, { file_extension = data })
--Log("this msg will be logged and written immediately in /script-output/YourModName/test.data!")
--
-- @tparam[opt='log'] string log_name the name of the logger
-- @tparam[opt=false] boolean debug_mode toggles the debug state of logger
-- @tparam[opt={...}] options options a table with optional arguments
-- @return (<span class="types">@{Logger}</span>) the logger instance
function Logger.new(log_name, debug_mode, options)
local mod_name = script and script.mod_name or 'Data'
log_name = log_name or 'log'
Logger._loggers[log_name] = nil
options = options or {}
local Log = {
__class = 'Log',
mod_name = mod_name,
log_name = log_name,
debug_mode = debug_mode,
buffer = {},
last_written = 0,
ever_written = false,
}
---
-- Used in the @{new} function for logging game ticks, specifying logfile extension, or forcing the logs to append to the end of the logfile.
-- @tfield[opt=false] boolean log_ticks whether to include the game tick timestamp in the logs
-- @tfield[opt="log"] string file_extension a string that overrides the default logfile extension
-- @tfield[opt=false] boolean force_append if true, every new message appends to the current logfile instead of creating a new one
-- @table Log.options
Log.options = {
log_ticks = options.log_ticks or false,
file_extension = options.file_extension or 'log',
force_append = options.force_append or false
}
Log.file_name = mod_name .. '/' .. log_name .. (log_name:find('%.') and '' or '.' .. Log.options.file_extension)
Log.ever_written = Log.options.force_append
--- Logs a message.
-- @tparam string|table msg the message to log. @{table}s will be dumped using [serpent](https://github.com/pkulchenko/serpent)
-- which is included in the official Factorio Lualib
-- @return (<span class="types">@{Logger}</span>) the logger instance
-- @see https://forums.factorio.com/viewtopic.php?f=25&t=23844 Debugging utilities built in to Factorio
function Log.log(msg)
if type(msg) ~= 'string' then
msg = serpent.block(msg, { comment = false, nocode = true, sparse = true })
end
if _G.game then
local tick = game.tick
local floor = math.floor
local time_s = floor(tick / 60)
local time_minutes = floor(time_s / 60)
local time_hours = floor(time_minutes / 60)
if Log.options.log_ticks then
table.insert(Log.buffer, format('%02d:%02d:%02d.%02d: %s\n', time_hours, time_minutes % 60, time_s % 60, tick - time_s * 60, msg))
else
table.insert(Log.buffer, format('%02d:%02d:%02d: %s\n', time_hours, time_minutes % 60, time_s % 60, msg))
end
-- write the log every minute
if (Log.debug_mode or (tick - Log.last_written) > 3600) then
return Log.write()
end
else
if _G.script then --buffer when a save is loaded but _G.game isn't available
if Log.options.log_ticks then
table.insert(Log.buffer, format('00:00:00:00: %s\n', msg))
else
table.insert(Log.buffer, format('00:00:00: %s\n', msg))
end
else --log in data stage
log(format('%s/%s: %s', Log.mod_name, Log.log_name, msg))
end
end
return Log
end
--- Writes out all buffered messages immediately.
-- @return (<span class="types">@{Logger}</span>) the logger instance
function Log.write()
if _G.game and table.size(Log.buffer) > 0 then
Log.last_written = game.tick
game.write_file(Log.file_name, table.concat(Log.buffer), Log.ever_written)
Log.buffer = {}
Log.ever_written = true
end
return Log
end
setmetatable(Log, _Log_mt)
Logger._loggers[log_name] = Log
return Log
end
return Logger

View File

@@ -0,0 +1,41 @@
--- Migration helper functions
-- @module Misc.Migrate
local Migrate = {
__class = 'Migrate',
__index = require('__stdlib__/stdlib/core')
}
setmetatable(Migrate, Migrate)
local Is = require('__stdlib__/stdlib/utils/is')
--- Migrate a dictionary of recipe -> tech names
-- @tparam dictionary dictionary
function Migrate.Recipes(dictionary)
Is.Assert.Table(dictionary, 'dictionary of recipes->technology not found')
for _, force in pairs(game.forces) do
for recipe, tech in pairs(dictionary) do
if force.technologies[tech] and force.technologies[tech].researched then
if force.recipes[recipe] then
force.recipes[recipe].enabled = true
end
end
end
end
end
function Migrate.all_recipes()
for _, force in pairs(game.forces) do
for _, tech in pairs(force.technologies) do
if tech.researched then
for _, unlock in pairs(tech.effects or {}) do
if unlock.type == 'unlock-recipe' then
force.recipes[unlock.recipe].enabled = true
end
end
end
end
end
end
return Migrate

View File

@@ -0,0 +1,362 @@
--- A double queue.
-- Taken from ***Programming in Lua*** [Queues and Double Queues](http://www.lua.org/pil/11.4.html)
-- and modified to not allow nil values, and returns nil if @{pop_first} or @{pop_last} is used when the queue is empty.
-- @module Misc.Queue
-- @usage local Queue = require('__stdlib__/stdlib/misc/queue')
-- local q = Queue() -- create a new empty queue
-- q('my value') -- push a value onto the queue
-- q() -- pop the last value off the queue
-- game.print(#q) -- print the number of items in the queue
local Queue = {
__class = 'Queue',
__index = require('__stdlib__/stdlib/core')
}
setmetatable(Queue, Queue)
local table = require('__stdlib__/stdlib/utils/table')
local t_size = table_size
local Inspect = require('__stdlib__/stdlib/vendor/inspect')
local meta = {}
function Queue.__call(_, ...)
local queue = { first = 1, last = 0, objects = {} }
setmetatable(queue, meta)
for _, push in pairs { ... } do
queue(push)
end
return queue
end
--- Constructs a new Queue object.
-- @param ... mixed, values to push into the queue
-- @treturn @{Queue} a new queue
function Queue.new(...)
return Queue.__call(nil, ...)
end
--- Load global.queue or queues during on_load, as metatables are not persisted.
-- <p>This is only needed if you are using the queue as an object and storing it in global.
-- @tparam table queue (<span class="types">@{Queue}</span>,...)
-- @usage global.myqueue1 = Queue.new()
-- script.on_load(function() Queue.load(global.myqueue))
function Queue.load(queue)
if type(queue) == 'table' and queue.first then
return setmetatable(queue, meta)
end
end
--- Push a new element to the front of the queue.
-- @tparam Queue queue the queue to push an element to
-- @tparam Mixed value the element to push
function Queue.push_first(queue, ...)
for _, value in pairs { ... } do
queue.first = queue.first - 1
queue.objects[queue.first] = value
end
return queue
end
--- Push a new element to the back of the queue.
-- @tparam Queue queue the queue to push an element to
-- @tparam Mixed ... the element(s) to push
function Queue.push_last(queue, ...)
for _, value in pairs { ... } do
queue.last = queue.last + 1
queue.objects[queue.last] = value
end
return queue
end
--- Shortcut for @{Queue.push_last}
-- @function Queue.push
Queue.push = Queue.push_last
--- Push a new element to a specific location of the queue.
-- @tparam Queue queue the queue to push an element to
-- @tparam number index the index to push to.
-- @tparam Mixed value the element to push.
function Queue.push_at(queue, index, value)
if index < queue.first then
return Queue.push_first(queue, value)
elseif index > queue.last then
return Queue.push_last(queue, value)
else
table.insert(queue.objects, index, value)
queue.last = queue.last + 1
end
return queue
end
function Queue.wrapper(self, func_name, ...)
if Queue[func_name] then
Queue[func_name](self, ...)
end
return self
end
--- Retrieve the element at the front of the queue and remove it from the queue.
-- @tparam Queue queue the queue to retrieve the element from
-- @treturn Mixed value the element at the front of the queue
function Queue.pop_first(queue)
if Queue.is_empty(queue) then
return nil
end
local first = queue.first
local value = queue.objects[first]
queue.objects[first] = nil -- to allow garbage collection
queue.first = first + 1
return value
end
--- Shortcut for @{Queue.pop_first}
-- @function Queue.pop
Queue.pop = Queue.pop_first
local function remove(queue, index)
local ret = queue.objects[index]
if ret ~= nil then
for i = index + 1, queue.last do
queue.objects[i - 1] = queue.objects[i]
end
queue.objects[queue.last] = nil
queue.last = queue.last - 1
end
return ret
end
--- Pop an element at a specific location of the queue.
-- @tparam Queue queue the queue to push an element to
-- @tparam number index the index to push to.
-- @treturn Mixed value the popped element.
function Queue.pop_at(queue, index)
return remove(queue, index)
end
--- Peek at an element in the queue without disturbing the queue.
-- @tparam Queue queue the queue to peek at
-- @tparam number index the index in the queue to peek at
-- @treturn Mixed the value of the peeked element
function Queue.peek_at(queue, index)
return queue.objects[index]
end
--- Return the element at the front of the queue and remove it from the queue.
-- @tparam Queue queue the queue to retrieve the element from
-- @treturn Mixed the element at the front of the queue
function Queue.peek_first(queue)
return queue.objects[queue.first]
end
--- Shortcut for @{Queue.peek_first}
-- @function Queue.peek
Queue.peek = Queue.peek_first
--- Retrieve the element at the back of the queue and remove it from the queue.
-- @tparam Queue queue the queue to retrieve the element from
-- @treturn Mixed the element at the back of the queue
function Queue.pop_last(queue)
if queue.is_empty(queue) then
return nil
end
local last = queue.last
local value = queue.objects[last]
queue.objects[last] = nil -- to allow garbage collection
queue.last = last - 1
return value
end
--- Return the element at the back of the queue.
-- @tparam Queue queue the queue to retrieve the element from
-- @treturn Mixed the element at the back of the queue
function Queue.peek_last(queue)
return queue.objects[queue.last]
end
--- Returns the popped value and pushes back into the queue.
-- @tparam Queue queue the queue
-- @return Mixed the value that was popped.
function Queue.pop_and_push(queue)
local ret = queue.pop(queue)
queue.push(queue, ret)
return ret
end
--- Returns the queue after popping the last element and pushing it to the top.
-- @tparam Queue queue the queue
-- @treturn @{Queue} the queue
function Queue.cycle(queue)
return queue.push(queue, queue.pop(queue))
end
--- Gets the first index which matches the stored data. does not compare inside tables.
function Queue.find(queue, find)
for i, v in pairs(queue) do
if v == find then
return i
end
end
end
local function _sort_func(a, b)
local lhs = type(a) == 'table' and '{' or tostring(a)
local rhs = type(b) == 'table' and '{' or tostring(b)
return lhs < rhs
end
--- sort and reorder the queue
function Queue.sort(queue, func)
local sorted = {}
for _, v in pairs(queue) do
if v ~= nil then
sorted[#sorted + 1] = v
end
end
table.sort(sorted, func or _sort_func)
queue.objects = sorted
queue.first, queue.last = 1, #queue.objects
return queue
end
--- Returns true if the given queue is empty.
-- @tparam Queue queue the queue to check
-- @treturn boolean true if empty, false otherwise
function Queue.is_empty(queue)
return queue.first > queue.last
end
--- Returns the number of items in the queue.
-- @tparam Queue queue the queue to check
-- @treturn number the number of items in the queue
function Queue.size(queue)
return t_size(queue.objects)
end
--- Shortcut for @{Queue.size}
-- @function Queue.count
Queue.count = Queue.size
--- Return the next element in the queue
-- @tparam Queue queue the queue to check
-- @tparam number|nil index if nil return the first value, else return the next index value
-- @tparam boolean pop pop the value off the queue
-- @treturn number|nil the index
-- @treturn Mixed|nil the value at queue index
function Queue.next(queue, index, pop)
index = not index and queue.first or index + (pop and 0 or 1)
for i = index, queue.last do
local v = queue.objects[i]
if v ~= nil then
return i, pop and Queue.pop_at(queue, i) or v
end
end
return nil, nil
end
--- Return the previous element in the queue
-- @tparam Queue queue the queue to check
-- @tparam number|nil index if nil return the last value, else return the previous index value
-- @tparam boolean pop pop the value off the queue
-- @treturn number|nil the index
-- @treturn Mixed|nil the value at queue index
function Queue.rnext(queue, index, pop)
-- next returns index of next or nil and data,
index = not index and queue.last or (index < queue.first and queue.first or index) - 1
for i = index, queue.first, -1 do
local v = queue.objects[i]
if v ~= nil then
return i, pop and Queue.pop_at(queue, i) or v
end
end
return nil, nil
end
local function next_pop(queue, index)
return Queue.next(queue, index, true)
end
local function rnext_pop(queue, index)
return Queue.rnext(queue, index, true)
end
--- Iterate the queue forward
function Queue.pairs(queue, pop)
return pop and next_pop or Queue.next, queue, nil
end
--- Iterate the queue backwards
function Queue.rpairs(queue, pop)
return pop and rnext_pop or Queue.rnext, queue, nil
end
do
meta.__class = 'queue'
meta.__len = Queue.size
meta.__unm = Queue.pop
meta.__parent = Queue
meta.__debugline = [[<Queue>{[}first={first},last={last}{]}]]
-- Allows queue[3] to return the item at queue.objects[3]
meta.__index = function(self, k)
if type(k) == 'number' then
return self:peek_at(k)
else
local v = rawget(self, k)
if v == nil then
return Queue[k]
end
return v
end
end
meta.__newindex = function(self, k, v)
if type(k) == 'number' then
if v ~= nil then
self:push_at(k, v)
else
error('Attempt to modify Queue structure')
end
else
rawset(self, k, v)
end
end
-- Allows queue() to pop_first and queue(data) to push_last
meta.__call = function(self, ...)
if ... then
return self:push(...)
else
return self:pop()
end
end
meta.__tostring = function(self)
return Inspect({ first = self.first, last = self.last, objects = self.objects }, { arraykeys = true })
end
meta.__add = function(queue1, queue2)
local new = Queue.new()
local lhs = getmetatable(queue1) == meta and true
local rhs = getmetatable(queue2) == meta and true
if lhs then
for _, v in pairs(queue1.objects) do
new:push(v)
end
else
new:push(queue1)
end
if rhs then
for _, v in pairs(queue2.objects) do
new:push(v)
end
else
new:push(queue2)
end
return new
end
end
return Queue

View File

@@ -0,0 +1,61 @@
--[[
A basic interface script, with generic functions
usage:
local interface = require(__stdlib__/stdlib/scripts/interface)
interface.myfunc = function() end
remote.add_interface(script.mod_name, interface)
interface.myfunc2 = function() end -- Can even add new functions afterwards!
]] --
local interface = {}
local Table = require('__stdlib__/stdlib/utils/table')
local Event = require('__stdlib__/stdlib/event/event')
local Game = require('__stdlib__/stdlib/game')
local Changes = require('__stdlib__/stdlib/event/changes')
local Player = require('__stdlib__/stdlib/event/player')
local Force = require('__stdlib__/stdlib/event/force')
local ignore_defines = Table.invert { 'anticolor', 'lightcolor', 'color', 'time' }
local function write(data, name, keyignore, maxlevel)
return serpent.block(data, { comment = false, nocode = true, name = name, keyignore = keyignore, maxlevel = maxlevel })
end
interface['write_global'] = function()
game.remove_path(script.mod_name)
game.write_file(script.mod_name .. '/global.lua', write(global, 'global'))
game.write_file(script.mod_name .. '/package.lua', write(package.loaded, 'package', nil, 1))
game.write_file(script.mod_name .. '/interface.lua', write(remote.interfaces[script.mod_name] or {}, 'interface'))
game.write_file(script.mod_name .. '/_G.lua', write(_G, 'globals', nil, 1))
Event.dump_data()
Player.dump_data()
Force.dump_data()
Changes.dump_data()
end
interface['dump_all'] = function()
game.remove_path('Interfaces')
game.write_file('defines.lua', write(defines, 'defines', ignore_defines))
game.write_file('interfaces.lua', write(remote.interfaces, 'interfaces'))
for name, interfaces in pairs(remote.interfaces) do
-- Write each interface
game.write_file('Interfaces/' .. name .. '.lua', write(interfaces, 'interface'))
if interfaces['write_global'] then remote.call(name, 'write_global') end
end
Game.write_mods()
Game.write_surfaces()
Game.write_statistics()
game.print('Finished writing all data to script-output')
end
interface['merge_interfaces'] = function(tab)
Table.merge(interface, tab, false)
return interface
end
return interface

View File

@@ -0,0 +1,262 @@
--- Quickstart script.
-- Creates a quickstart world for testing mods.
-- <p>config settings should be a created in a table retured from `config-quickstart.lua`
-- @script quickstart
-- @usage
-- -- For use with STDLIB Events
-- if DEBUG then
-- require('__stdlib__/stdlib/scripts/quickstart').register_events()
-- end
-- @usage
-- --If not using stdlibs event system
-- local quickstart = require('__stdlib__/stdlib/scripts/quickstart')
-- script.on_event(defines.events.on_player_created, function()
-- quickstart.on_player_created() -- can be wrapped in an if DEBUG type check
-- end)
local Event = require('__stdlib__/stdlib/event/event')
local Area = require('__stdlib__/stdlib/area/area')
local QS = require('__stdlib__/stdlib/misc/config').new(_ENV.prequire('config-quickstart') or {})
if not remote.interfaces['quickstart_script'] then
local qs_interface = {}
qs_interface.registered_to = function()
if game then
game.print(script.mod_name)
end
return script.mod_name
end
remote.add_interface('quickstart-script', qs_interface)
else
return
end
local quickstart = {}
local rolling_stock = {
['locomotive'] = true,
['cargo-wagon'] = true,
['artillery-wagon'] = true,
['fluid-wagon'] = true
}
function quickstart.on_init()
if not game.is_multiplayer() then
if remote.interfaces['freeplay'] then
remote.call('freeplay', 'set_skip_intro', true)
remote.call('freeplay', 'set_disable_crashsite', true)
local items = QS.get('items', {})
remote.call('freeplay', 'set_created_items', items)
remote.call('freeplay', 'set_respawn_items', items)
elseif remote.interfaces['sandbox'] then
remote.call('sandbox', 'set_skip_intro', true)
remote.call('sandbox', 'set_skip_cheat_menu', true)
end
end
end
function quickstart.on_player_created(event)
if not game.is_multiplayer() then
local player = game.players[event.player_index]
local surface = player.surface
local force = player.force
local area = Area(QS.get('area_box', { { -100, -100 }, { 100, 100 } })):shrink_to_surface_size(surface)
local chart_size_multiplier = QS.get('chart_size_multiplier', 0)
if chart_size_multiplier > 0 then
player.force.chart(surface, Area(area, true) * chart_size_multiplier--[[@as BoundingBox]] )
end
player.surface.always_day = QS.get('always_day', false) --[[@as boolean]]
if QS.get('cheat_mode', false) then
player.cheat_mode = true
player.force.research_all_technologies()
if player.character then
player.character_running_speed_modifier = 2
player.character_reach_distance_bonus = 200
player.character_build_distance_bonus = 200
end
end
local power_armor = QS.get('power_armor', 'fake')
if player.character and game.item_prototypes[power_armor] then
--Put on power armor, install equipment
player.get_inventory(defines.inventory.character_armor).insert(power_armor)
local grid = player.character.grid
if grid then
for _, eq in pairs(QS.get('equipment', { 'fusion-reactor-equipment' })) do
if game.equipment_prototypes[eq] then
grid.put { name = eq }
end
end
end
end
if remote.interfaces['RSO'] then
if QS.get('disable_rso_starting', false) and remote.interfaces['RSO']['disableStartingArea'] then
remote.call('RSO', 'disableStartingArea')
end
if QS.get('disable_rso_chunk', false) and remote.interfaces['RSO']['disableChunkHandler'] then
remote.call('RSO', 'disableChunkHandler')
end
end
if QS.get('destroy_everything', false) then
for _, entity in pairs(surface.find_entities(area)) do
-- destroying cliffs can invalidate other cliffs so .valid is needed here
if entity.valid and entity.name ~= 'character' then
entity.destroy()
end
end
end
if QS.get('floor_tile', false) then
local tiles = {}
local floor_tile = QS.get('floor_tile')
local floor_tile_alt = QS.get('floor_tile_alt', floor_tile)
for x, y in Area(area):spiral_iterate() do
if y % 2 == 0 then
if x % 2 == 0 then
tiles[#tiles + 1] = { name = floor_tile, position = { x = x, y = y } }
else
tiles[#tiles + 1] = { name = floor_tile_alt, position = { x = x, y = y } }
end
else
if x % 2 ~= 0 then
tiles[#tiles + 1] = { name = floor_tile, position = { x = x, y = y } }
else
tiles[#tiles + 1] = { name = floor_tile_alt, position = { x = x, y = y } }
end
end
end
surface.set_tiles(tiles, true)
surface.destroy_decoratives(area)
end
if QS.get('ore_patches', false) then
--Top left
for x, y in Area { { -37.5, -27.5 }, { -33.5, -3.5 } }:iterate() do
surface.create_entity { name = 'coal', position = { x, y }, amount = 2500 }
end
--Top Right
for x, y in Area { { 33.5, -27.5 }, { 37.5, -3.5 } }:iterate() do
surface.create_entity { name = 'iron-ore', position = { x, y }, amount = 2500 }
end
--Bottom Right
for x, y in Area { { 33.5, 3.5 }, { 37.5, 27.5 } }:iterate() do
surface.create_entity { name = 'copper-ore', position = { x, y }, amount = 2500 }
end
--Bottom Left
for x, y in Area { { -37.5, 3.5 }, { -33.5, 27.5 } }:iterate() do
surface.create_entity { name = 'stone', position = { x, y }, amount = 2500 }
end
surface.create_entity { name = 'crude-oil', position = { -35.5, 1.5 }, amount = 32000 }
surface.create_entity { name = 'crude-oil', position = { -35.5, -1.5 }, amount = 32000 }
surface.create_entity { name = 'crude-oil', position = { 35.5, 1.5 }, amount = 32000 }
surface.create_entity { name = 'crude-oil', position = { 35.5, -1.5 }, amount = 32000 }
end
if QS.get('water_tiles', false) then
local water_tiles = {}
for x = 27.5, -27.5, -1 do
for y = 45.5, 50.5, 1 do
if x < -4 or x > 4 then
water_tiles[#water_tiles + 1] = {
name = 'water',
position = { x = x, y = y }
}
end
end
end
surface.set_tiles(water_tiles, false)
end
if QS.get('chunk_bounds', false) then
local black = { r = 0, g = 0, b = 0 }
-- Horizontal
rendering.draw_line { width = 2.0, color = black, from = { x = 32, y = 0 }, to = { x = -32, y = 0 }, surface = surface }
rendering.draw_line { width = 2.0, color = black, from = { x = 32, y = 32 }, to = { x = -32, y = 32 }, surface = surface }
rendering.draw_line { width = 2.0, color = black, from = { x = 32, y = -32 }, to = { x = -32, y = -32 }, surface = surface }
-- Vertical
rendering.draw_line { width = 2.0, color = black, from = { x = 0, y = 32 }, to = { x = 0, y = -32 }, surface = surface }
rendering.draw_line { width = 2.0, color = black, from = { x = 32, y = 32 }, to = { x = 32, y = -32 }, surface = surface }
rendering.draw_line { width = 2.0, color = black, from = { x = -32, y = 32 }, to = { x = -32, y = -32 }, surface = surface }
-- Center
rendering.draw_circle { width = 2.0, color = black, surface = surface, radius = 1, filled = false, target = { x = 0, y = 0 } }
end
-- Create proxy blueprint from string, read in the entities and remove it.
local bpstring
local custom_string = QS.get('custom_bp_string', false) --[[@as string]]
if custom_string then
bpstring = custom_string
elseif QS.get('default_bp_string', false) then
bpstring = quickstart.bpstring
end
if bpstring then
local bp = surface.create_entity { name = 'item-on-ground', position = { 0, 0 }, force = force, stack = 'blueprint' }
bp.stack.import_stack(bpstring)
local revive = bp.stack.build_blueprint { surface = player.surface, force = player.force, position = { 0, 2 }, force_build = true,
skip_fog_of_war = false, by_player = player }
local count = #revive
for i, ent in ipairs(revive) do
-- put rolling stock at the end.
if i < count and rolling_stock[ent.ghost_type] then
revive[#revive + 1] = ent
else
if ent.ghost_type == 'locomotive' then
local _, loco = ent.revive()
loco.burner.currently_burning = 'rocket-fuel' ---@diagnostic disable-line: assign-type-mismatch
loco.burner.remaining_burning_fuel = 222222222
else
ent.revive()
end
end
end
bp.destroy()
end
if QS.get('setup_power', false) then
if game.entity_prototypes['debug-energy-interface'] then
local es = surface.create_entity { name = 'debug-energy-interface', position = { 0, 0 }, force = force, raise_built = true }
es.destructible = false
end
if game.entity_prototypes['debug-substation'] then
local sb = surface.create_entity { name = 'debug-substation', position = { 0, 0 }, force = force, raise_built = true }
sb.destructible = false
end
end
end
end
---@diagnostic disable
quickstart.bpstring =
[[
0eNqtnc1v20YQxf8X9ioZnFl++tZDgJ4aoEnRQ2EEikTbRGXJoOikbuD/vZTF+EMZWvP2JSfL0L5Zcn/zdpZDxN+Sz+u75rZrN31y/i1pl9vNLjn/+1uya682i/X+d/39bZOcJ23f3CSzZLO42X/qFu06eZgl7Wb
V/Jucy8PFLGk2fdu3zWH844f7T5u7m89NN3zh1cj5qD5Lbre7Ycx2sw806My1PMtnyf3wUxbO8iHAqu2a5eEb+jD7QVefdPtBeDPf9dtbS7YYRbPXksOvd/3i8HPyy+/v//j4W2JECU9Rlnfdl2Y1f7z8H8OEMIa
RozCGZvakudtP/eq6n1LValTV0/cj96sWftXCr5r5VUu/qvpVK79q6let3aoCrJakfllguUT8ssB6ifplgQWT4JcFVkz8CYasmD/BkAXzJxiyXkCCAaruBEMWy51fwEzVnV3ATVV3bgHrr+7MAlBVd14haaXutEI
8QN1phRiWutMKcVd1pxWyFag7rZBtS/15BaxWcCcWUg4Ed2YhpUtQX+22j26VbsHS9O9YYZxqqF7LlpasO7VCZquKpZp7i9d6FM0dooXzrpZPmpaKf4PKRuhDagq5kydUb+rUvmNKqL+fUrQ+PqVkVq2fwheqlTX
BTNALndDxnZteTObVQendrx8+WuekLODXWZjzy+DrtHVyfEKZKVTAE7J1cORVTSEY+QmdGp+QmTt5Ck/I1hF4QmIynis6oQkdHGoxYcxhqCd0cKjFhDGHoZ7QwaEWE8YchnpCB4daTBgLGOoJHRxqk8UCZtqWwZE
2SSxgom0ZHGiTwwLm2ZaJ8GhTB7doUwaG2USwRFG2VWCQzWsqUY5tFRhjc71LlGJbBYbYzIUSZdhWgRE2faJECbZVYIBtE61QgidkYITtTaZCGZ6QgSG2N+EKpXhCBsbYLlIqlOMJGRhku4irUJInZGCU7SK3RlG
ekBFf+ys7PNzRo+cl2atD3V/v7ENdrfA1m6DXAb1mWyZzthIzGQ/puu8kWkow6vaZrkZRn5ApsQc1Wp9+8FBX0HOaY8nckqzRh19BT6tKmmI91uOnVKUpKtDDv5B7WnYKPqU7vvxgqga0HRwKz2QztB/sk83RhrB
PtkA7wj7ZEm0J+2QrtCfsk63RnrBLFmk1A0uGtJqBJUNazcCSAa1mYMWATjOyYDnYE/apFmBP2Kf6nGGLrm/X66a7n39dXA0DJrrCIT+r6yKtQ7n33G3XDpJjJZKeqblDoI1n38zBxrNL9EXj+XJ9166mbkX5LOm
6A/7eM0CEv/cMwOvvPQN5BrSeAQKA1jOCwIsG2aK72k4hIOEpD2LzAuhHI1hUYO/cp/qcbuvtcnuz7dsvjZFrAmaGvyONbMVARxoALijYk/epBrAn71PNwJ68T/U55W7vbqy+43hyC+YhXF50n+3xxYnx5Ynx1Yn
xlevY/f1GHzXp9dWp+8P7P+13TsXbjNb8+2zL4150Yb4X95wr28vL3fW2a+b2bXh+GzfLPG1uedGePiVdgMrqVc5A4eAVfrrPlvLFsKjL62Z1tx7ff362t/3nwfhefOPwgvUxA7Pk66LtPy23m9Vj2IPOoHK76Jp
P41vYi81q+OL44fJuvU4eLvaXddycx8X69mbvxH27/Ge3f/h/LHt4Ofrn6z4+dvoZshf7RRiKz3EBfnhu8P2dmb1DjSt+vfhv0a3mQ9hl1/TNvNsb3GMyTo7OqdEZNTpQo5UaLdTolBn9+FQqfnRFjS6p0RRrSrG
mFGtKsaYUa0qxphRrQrEmFGtCsSYUa0KxJhRrQrEmFGtCsSYUaxRqFGkUaBRnFGbc7kmZCkUJMZgBjInLXDBzp5klZthioGayiUljxj8o56JMk/Jraqugdilqg6T2ZqosoCoSqhii6jCqBKSqT6rwpWpuqtynThr
UIYc6X0Ue7QJ1lA9MaKVCKxNaqNDChE6p0CkRWmsmtNZM6IoKXTGhSyp0yYQuqNAFEzqnQudMaOrpoDJuppSbKeNmSrmZMm6mlJsp42ZKuZkybiaUmwnjZkK5mTBuJpSbCeNmQrmZMG4mlJsJ42ZCuZkwbiaUmwn
jZkK5mTBuJpSbCeNmQrmZMG5GmRnjZZSVMU5GGRnjY5SNMS5GmRjjYZSFUadL6qzD1CVUWcJYyOnA6+byrbFvxZ0amsZHTaODEpcaf6UaH1Sjg4b4oCE6aBYfNIsOmscHzaODFvFBi+igZXzQMjpoFR+0ig5axwe
t482BsCQhPIkxpXhXEsKWJN6XhDAmiXcmIaxJ4r1JCHOSeHcSwp4k3p+EMCiJdyghLEriPUoIk5J4l1LCpTTepZRwKSVqJ6Z4incpJVxK411KCZfSeJdSwqU03qWUcCmNdyklXErjXUoJl9J4l1LCpTTepQLhUiH
epQLhUk9jc2JsRowNxFglxgoxNo0fy6zvW28dnBxbEmMJrpTgSgmulOBKCa6U4EoJroTgSgiuhOBKCK6E4EoIroTgSgiuhOBKCK4IrAiqCKgIpgikmB2QMAyCieih8TDFx4y/0Pi7G7+k8RzFwxufMfFpGu8NhCM
RRkj4L2H7xG5DbHLE3kps6UQlQRQwRN1ElGtElUgUp0RNTJTixAmAOHgQ553Tx6yL2eGve52/+GNgs+RL0+0O/7VaSCUb/snA5cP/+AhWDQ==
]]
---@diagnostic enable
function quickstart.register_events()
Event.register(Event.core_events.on_init, quickstart.on_init)
Event.register(defines.events.on_player_created, quickstart.on_player_created)
return quickstart
end
return quickstart

View File

@@ -0,0 +1,520 @@
--- A double-linked list implementation
-- @classmod LinkedList
local Core = require('__stdlib__/stdlib/core')
local table = require('__stdlib__/stdlib/utils/table')
local Is = require('__stdlib__/stdlib/utils/is')
-- dumb shallow copy suitable for cloning instance metatables in subclasses
local _mtcopy = function(self)
local result = {}
for k, v in pairs(self._mt) do
result[k] = v
end
return result
end
-- @class LinkedListNode
-- @usage local llnode = linkedlist.append(item)
local LinkedListNode = setmetatable(
{
__class = 'linked_list',
__class_name = 'LinkedListNode',
_is_LinkedListNode = true,
_mt = {},
_mtcopy = _mtcopy
},
{
__index = Core
}
)
LinkedListNode._mt.__index = LinkedListNode
LinkedListNode.__class = LinkedListNode
-- @module linked_list
-- @usage local LinkedList = require('stdlib.utils.classes.linked_list')
local LinkedList = setmetatable(
{
__class = 'linked_list',
__class_name = 'LinkedList',
_is_LinkedList = true,
_node_class = LinkedListNode,
_mt = {},
_mtcopy = _mtcopy
},
{
__index = Core
}
)
LinkedList.__class = LinkedList
function LinkedList.new(self)
-- support LinkedList.new() syntax compatible with most stdlib classes
if Is.Nil(self) then self = LinkedList end
-- determine if this is a class or an instance; if an instance, assume they intended
-- to create a node and provide a hopefully-not-too-confusing error message.
Is.Assert.Nil(self.next, function()
return 'Use foo:new_node(), not foo:new(), to create new ' .. self.__class_name .. ' nodes'
end)
local result = setmetatable({ __class = self }, self._mt)
-- live_iterators is a set/bag (see _Programming_In_Lua_ 1st Ed. §11.5). It uses weak keys
-- so garbage collected iterators will be automatically removed (see :new_node_iter below).
result.live_iterators = setmetatable({}, { __mode = 'k' })
result.next = result
result.prev = result
return result
end
function LinkedList:new_node(item, prev, next)
-- only way to determine if this is a class or an instance
Is.Assert.Not.Nil(self, 'Use foo:new_node, not foo.new_node to create new nodes')
-- Retrieve the node class from the class if we are an instance
local node_class = Is.Nil(self.next) and self._node_class
or self.__class and self.__class._node_class
or error('LinkedList:new_node: cannot find node class, improper invocation?')
local result = setmetatable({ __class = node_class }, node_class._mt)
result.next = next or result
result.prev = prev or result
result.item = item
result.owner = self
return result
end
function LinkedList:from_stack(stack, allow_insane_sparseness)
Is.Assert.Not.Nil(self.__class, [[LinkedList:from_stack is a class method, not a static function; \z
For example LinkedList:from_stack(stack) would be a correct invocation syntax']])
-- since linkedlists effectively support sparse keys, ensure we can
-- round-trip various configurations by supporting sparse pseudo-stacks
local sparse_keys = {}
for k in pairs(stack) do
if type(k) == 'number' and math.floor(k) == k and math.max(1, k) == k then
table.insert(sparse_keys, k)
else
log('LinkedList.from_stack ignoring non-stackish key value "' .. tostring(k) .. '"')
end
end
table.sort(sparse_keys)
local result = self.__class:new()
-- subclasses could theoretically override the allow_insane_sparseness
-- object-level override in __class:new(), so respect their wishes here.
result.allow_insane_sparseness = result.allow_insane_sparseness or allow_insane_sparseness
local last_key = 0
for _, k in ipairs(sparse_keys) do
last_key = last_key + 1
if last_key < k then
if k - last_key >= 999 then
Is.Assert(result.allow_insane_sparseness, function()
return 'Refusing to create insanely sparse list at key ' .. tostring(k)
end)
end
repeat
last_key = last_key + 1
result.prev.next = result:new_node(nil, result.prev, result)
result.prev = result.prev.next
until last_key == k
end
result.prev.next = result:new_node(stack[k], result.prev, result)
result.prev = result.prev.next
end
return result
end
function LinkedList:to_stack()
local result = {}
local index = 1
for node in self:nodes() do
if Is.Not.Nil(node.item) then
result[index] = node.item
end
index = index + 1
end
return result
end
function LinkedList:length()
local result = 0
for _ in self:nodes() do
result = result + 1
end
return result
end
LinkedList._mt.__len = LinkedList.length
function LinkedList:is_empty()
Is.Assert.Not.Nil(self, 'LinkedList.is_empty called without self argument', 3)
Is.Assert.Not.Nil(self.next, 'LinkedList.next property is missing: structural error or bad argument', 3)
return self.next == self
end
function LinkedList:last_node()
return self.prev ~= self and self.prev or nil
end
function LinkedList:last_item()
local node = self:last_node()
return node and node.item or nil
end
function LinkedList:first_node()
return self.next ~= self and self.next or nil
end
function LinkedList:first_item()
local node = self:first_node()
return node and node.item or nil
end
function LinkedList:concatenate(other)
if Is.Nil(other) then
return self:copy()
else
Is.Assert(other._is_LinkedList, 'cannot concatenate non-linked-list with linked-list')
end
local self_copy = self:copy()
if not other:is_empty() then
local other_copy = other:copy()
self_copy.prev.next = other_copy.next
other_copy.next.prev = self_copy.prev
self_copy.prev = other_copy.prev
other_copy.prev.next = self_copy
end
return self_copy
end
LinkedList._mt.__concat = LinkedList.concatenate
function LinkedList._mt.__index(self, k)
if type(k) ~= 'number' or math.floor(k) ~= k or k < 1 then
-- any non-special index goes to the class from here
return self.__class[k]
end
local count = 1
local node = self.next
while node ~= self do
if count == k then
return node.item
end
node = node.next
count = count + 1
end
end
function LinkedList._mt.__newindex(self, k, v)
if type(k) ~= 'number' or math.floor(k) ~= k or k < 1 then
-- any non-special index goes straight into the table (the class is
-- immutable, but the object may opt to override class functions)
return rawset(self, k, v)
end
local count = 1
local node = self.next
while node ~= self do
if count == k then
node.item = v
return nil
end
node = node.next
count = count + 1
end
-- They have requested a new node to be appended, perhaps with a certain
-- number of intervening empty nodes. But, would the request create an
-- insanely sparse index?
Is.Assert(k - count < 999 or self.allow_insane_sparseness,
'Setting requested index in linkedlist would create insanely sparse list')
repeat
-- this is a bit gross; we increment count one /exta/ time here, on the
-- first iteration; so now count == self.length + 2
count = count + 1
node = self:append(nil)
-- nb: count == self.length + 1
until count > k
node.item = v
end
function LinkedList:append(item)
self.prev.next = self:new_node(item, self.prev, self)
self.prev = self.prev.next
return self.prev
end
function LinkedList:prepend(item)
self.next.prev = self:new_node(item, self, self.next)
self.next = self.next.prev
return self.next
end
function LinkedList:insert(item, index)
if not index then
return self:append(item)
elseif index == 1 then
return self:prepend(item)
end
Is.Assert(type(index) == 'number' and math.floor(index) == index and index >= 1,
'LinkedList:insert with irregular index')
local length = self:length()
Is.Assert(index - length <= 999 or self.allow_insane_sparseness,
'LinkedList:insert would create insanely sparse list.')
if length + 1 < index then
repeat
length = length + 1
self:append(nil)
until length + 1 == index
return self:append(item)
else
local node = self
while index > 1 do
node = node.next
index = index - 1
end
node.next.prev = self:new_node(item, node, node.next)
node.next = node.next.prev
return node.next.prev
end
end
function LinkedList:remove(index)
Is.Assert.Not.Nil(self, 'LinkedList:remove called without self argument.', 3)
Is.Assert.Not.Nil(index, 'LinkedList:remove without index argument', 3)
Is.Assert(type(index) == 'number' and math.floor(index) == index and index >= 1,
'LinkedList:remove with irregular index argument.', 3)
if self:is_empty() then
return nil
end
local count = 1
local node = self.next
while node ~= self do
if count == index then
return node:remove()
else
count = count + 1
node = node.next
end
end
end
function LinkedListNode:graft_after(target)
Is.Assert.Not.Nil(target, 'LinkedListNode.graft_after: Missing node argument or not invoked as node:graft_after(target)', 3)
repeat
target = target.next
until not target.is_tombstone
self.next = target
self.prev = target.prev
target.prev = self
self.prev.next = self
end
function LinkedListNode:graft_before(target)
Is.Assert.Not.Nil(target, 'LinkedListNode.graft_after: Missing node argument or not invoked as node:graft_after(target)', 3)
repeat
target = target.prev
until not target.is_tombstone
self.prev = target
self.next = target.next
target.next = self
self.next.prev = self
end
function LinkedListNode:prune()
Is.Assert.Not.Nil(self, 'LinkedListNode.prune: Missing self argument (invoke as node:prune())', 3)
self.prev.next = self.next
self.next.prev = self.prev
for live_iterator in pairs(self.owner.live_iterators) do
if live_iterator.at == self then
-- if live_iterator.is_forward_iterator then
live_iterator.forced = self.prev
-- else
-- live_iterator.forced = self.next
-- end
end
end
return self
end
function LinkedListNode:remove()
Is.Assert.Not.Nil(self, 'LinkedListNode.remove: Missing self argument (invoke as node:remove())', 3)
Is.Assert.Not(self.is_tombstone, 'LinkedListNode.remove: Double-removal detected.', 3)
self.is_tombstone = true
return self:prune()
end
function LinkedList:clear()
Is.Assert.Not.Nil(self, 'LinkedList.clear: Missing self argument (invoke as list:clear())', 3)
-- don't pull the rug out from under live iterators; tombstone each node as applicable,
-- skipping any nodes that were already iterated.
for iterator in pairs(self.live_iterators) do
if iterator.at then
local iterator_at = iterator.at
iterator.at = nil
while iterator_at ~= self do
iterator_at.is_tombstone = true
iterator_at = iterator_at.next
end
end
end
self.prev = self
self.next = self
end
function LinkedListNode:_copy_with_to(copy_fn, other_node)
other_node.item = copy_fn(self.item)
end
function LinkedList:_copy_with_to(copy_fn, other)
local last_node = other
for self_node in self:nodes() do
last_node.next = self:new_node(nil, last_node, other)
last_node = last_node.next
self_node:_copy_with_to(copy_fn, last_node)
end
other.prev = last_node
end
function LinkedList:_copy_with(copy_fn)
-- LinkedList.new does not permit instance:new(), so use class
local result = self.__class:new()
self:_copy_with_to(copy_fn, result)
return result
end
local function identity(x)
return x
end
function LinkedList:copy()
return self:_copy_with(identity)
end
LinkedList.deepcopy = table.flexcopy
function LinkedList:new_node_iterator()
Is.Assert.Not.Nil(self, 'LinkedList:new_node_iterator called without self argument \z
(did you mean to use ":" instead of "."?)', 2)
local iteration_tracker = {}
self.live_iterators[iteration_tracker] = true
return function(linked_list, node)
Is.Assert.True(linked_list == self, 'Wrong Linked List provided to node iterator', 3)
local next_node = iteration_tracker.forced or node
iteration_tracker.forced = nil
-- if items have been removed during iteration, we may encounter
-- tombstones here. Once we reach the next non-tombstoned node,
-- we have found our way back to the remaining legitimate nodes
repeat
next_node = next_node.next
until not next_node.is_tombstone
next_node = (next_node ~= self and next_node or nil)
iteration_tracker.at = next_node
if next_node == nil then
-- Technically, we could skip this step and rely on the garbage
-- collector, but even so we'd need iteration_tracker to be an upvalue.
-- Anyhow, why wait for GC? We know we're done, now.
self.live_iterators[iteration_tracker] = nil
iteration_tracker = nil
end
return next_node
end
end
function LinkedList:nodes()
return self:new_node_iterator(), self, self
end
function LinkedList:items()
-- we "need" a closure here in order to track the node, since it is not
-- returned by the iterator.
local iter = self:new_node_iterator()
local node = self
return function()
-- not much we can do about nils here so ignore them
repeat
node = iter(self, node)
if node and node.item ~= nil then
return node.item
end
until node == nil
end
end
function LinkedList:ipairs()
local i = 0
local node = self
local iter = self:new_node_iterator()
-- we kind-of "need" a closure here or else we'll end up having to
-- chase down the indexed node every iteration at potentially huge cost.
return function()
repeat
i = i + 1
node = iter(self, node)
if node ~= nil and node.item ~= nil then
return i, node.item
end
until node == nil
end
end
LinkedList._mt.__ipairs = LinkedList.ipairs
function LinkedList:tostring()
local result = self.__class_name .. ':from_stack {'
local skipped = false
local firstrep = true
local count = 0
for node in self:nodes() do
count = count + 1
local node_rep
if node.tostring then
node_rep = node:tostring()
elseif Is.False(node.item) then
node_rep = 'false'
elseif node.item then
if Is.String(node.item) then
node_rep = '"' .. node.item .. '"'
else
node_rep = tostring(node.item)
end
end -- else it is nil and we skip it
if node_rep then
if not firstrep then
result = result .. ', '
else
firstrep = false
end
if skipped then
-- if any index has been skipped then we provide
-- explicit lua index syntax i.e., {[2] = 'foo'}
result = result .. '[' .. tostring(count) .. '] = '
end
result = result .. node_rep
else
skipped = true
end
end
return result .. '}'
end
LinkedList._mt.__tostring = LinkedList.tostring
function LinkedList:validate_integrity()
if self.next == self then
Is.Assert(self.prev == self, 'Empty LinkedList prev and next do not match', 2)
else
Is.Assert.Nil(self.item, 'LinkedList contains item in head node', 2)
Is.Assert.Not.Nil(self._is_LinkedList, 'LinkedList head node does not have _is_LinkedList', 2)
local iteration = 0
local i = self
local prev_i = self
local visited = {}
while i.next ~= self do
iteration = iteration + 1
local err_hint = ' (iteration #' .. tostring(iteration) .. ')'
visited[i] = true
i = i.next
Is.Assert(i.prev == prev_i, 'next.prev mismatch' .. err_hint, 2)
prev_i = i
Is.Assert.Not(visited[i], 'LinkedList contains node-loop' .. err_hint, 2)
Is.Assert(i._is_LinkedListNode, 'LinkedList contains LinkedList as node' .. err_hint, 2)
end
Is.Assert(self.prev == prev_i, 'LinkedList prev is not terminal node in .next chain', 2)
end
return true
end
return LinkedList

View File

@@ -0,0 +1,142 @@
--- String Array Metatable
-- For working with string arrays without duplicate values
-- @classmod string_array
local M = {
__class = 'string-array-class'
}
local metatable
--- Does this array contain name.
-- @tparam string name The string to find.
-- @treturn boolean string is in array
function M:has(name)
local type = type(name)
if type == 'table' then
for _, str in pairs(name) do
if not self:has(str) then
return false
end
end
return true
end
assert(type == 'string', 'name must be a string')
for _, str in ipairs(self) do
if str == name then
return true
end
end
return false
end
--- Add a string to the array if it doesn't exist in the array.
-- @tparam string name
-- @treturn self
function M:add(name)
local type = type(name)
if type == 'table' then
for _, str in pairs(name) do
self:add(str)
end
return self
end
assert(type == 'string', 'name must be a string')
for _, str in ipairs(self) do
if str == name then
return self
end
end
table.insert(self, name)
return self
end
--- Remove the string from the array if it exists.
-- @tparam string name
-- @treturn self
function M:remove(name)
local type = type(name)
if type == 'table' then
for _, str in pairs(name) do
self:remove(str)
end
return self
end
assert(type == 'string', 'name must be a string')
for i, str in ipairs(self) do
if str == name then
table.remove(self, i)
return self
end
end
return self
end
--- Toggles the passed name in the array by adding it if not present or removing it if it is.
-- @tparam string name
-- @treturn self
function M:toggle(name)
local type = type(name)
if type == 'table' then
for _, str in pairs(name) do
self:toggle(str)
end
return self
end
assert(type == 'string', 'name must be a string')
for i, str in ipairs(self) do
if str == name then
table.remove(self, i)
return self
end
end
table.insert(self, name)
return self
end
--- Clear the array returning an empty array object
-- @treturn self
function M:clear()
for i = #self, 1, -1 do
table.remove(self, i)
end
return self
end
--- Convert the array to a string
-- @treturn string
function M:tostring()
return table.concat(self, ', ')
end
--- Concat string-arrays and strings together
-- @tparam string|string-array rhs
-- @treturn string-array
function M:concat(rhs)
if getmetatable(self) == metatable then
return self:add(rhs)
else
return rhs:add(self)
end
end
--- The following metamethods are provided.
-- @table metatable
metatable = {
__index = M, -- Index to the string array class.
__tostring = M.tostring, -- tostring.
__concat = M.concat, -- adds the right hand side to the object.
__add = M.add, -- Adds a string to the string-array object.
__sub = M.remove, -- Removes a string from the string-array object.
__unm = M.clear, -- Clears the array.
__call = M.has -- Array contains this string.
}
return function(array)
if type(array) == 'table' then
return setmetatable(array, metatable)
end
end

View File

@@ -0,0 +1,338 @@
--- Unique Array class
-- For working with unique string array values. Creates an array with hash/dictionary indexing.
-- @classmod unique_array
-- Adding or removeing values without using the provided methods can break the functionality of this class.
-- Additional methods exported by requering unique_array are .set and .make_dictionary
-- @usage local Unique_Array = require('__stdlib__/stdlib/utils/classes/unique_array')
-- local my_array = Unique_Array('first')
-- my_array:add('second')
-- if my_array['second'] then
-- print('its truthy')
-- end'
-- @set all=true
local M = {
__class = 'unique_array'
}
local type, ipairs = type, ipairs
local getmetatable, setmetatable, rawset = getmetatable, setmetatable, rawset
local t_remove, t_sort = table.remove, table.sort
local function add(self, class, param)
local index = #self + 1
rawset(self, index, param)
class.dictionary[param] = index
class.len = class.len + 1
return index
end
local function dictionary_sort(self, class)
class.len = 0
for i, v in ipairs(self) do
class.len = class.len + 1
class.dictionary[v] = i
end
end
local function remove(self, class, param)
t_remove(self, class.dictionary[param])
class.dictionary[param] = nil
dictionary_sort(self, class)
end
local wrappers = {}
local function create_class(tab)
if type(tab) == 'table' then
local class = {
__concat = M.concat,
__tostring = M.tostring,
__eq = M.same,
__lt = wrappers.__lt,
__add = wrappers.__add,
__sub = wrappers.__sub,
__lte = wrappers.__lt,
len = 0
}
class.dictionary = M.make_dictionary(tab)
class.__index = function(_, k)
return class.dictionary[k] or M[k]
end
return setmetatable(tab, class)
end
end
local function unique_or_new(tab)
if type(tab) == table and tab.__class == 'unique_array' then
return tab
else
return M.new(tab)
end
end
wrappers.__lt = function(lhs, rhs)
lhs, rhs = unique_or_new(lhs), unique_or_new(rhs)
return rhs:all(lhs)
end
wrappers.__add = function(lhs, rhs)
return M.new(lhs):add(rhs)
end
wrappers.__sub = function(lhs, rhs)
return M.new(lhs):remove(rhs)
end
wrappers.__pairs = function(self)
local dictionary = getmetatable(self).dictionary
return next, dictionary, nil
end
--- Methods
-- @section Methods
--- Create a new unique array.
-- @tparam unique_array|string|{string,...} ... strings to initialize the unique array with
-- @treturn @{unique_array} new
function M.new(...)
return create_class {}:add(type((...)) == 'table' and (...) or { ... })
end
--- Add a string to the array if it doesn't exist in the array.
-- @tparam unique_array|string|{string,...} other
-- @treturn @{unique_array} self
function M:add(other)
local class = getmetatable(self)
for _, param in ipairs(type(other) == 'table' and other or { other }) do
if not self[param] then
add(self, class, param)
end
end
return self
end
--- Remove the strings from the array if they exist.
-- @tparam unique_array|string|{string,...} other
-- @treturn @{unique_array} self
function M:remove(other)
local class = getmetatable(self)
for _, param in ipairs(type(other) == 'table' and other or { other }) do
if self[param] then
remove(self, class, param)
end
end
return self
end
--- Toggles the passed name in the array by adding it if not present or removing it if it is.
-- @tparam unique_array|string|{string,...} other
-- @treturn @{unique_array} self
function M:toggle(other)
local class = getmetatable(self)
for _, param in ipairs(type(other) == 'table' and other or { other }) do
if self[param] then
remove(self, class, param)
else
add(self, class, param)
end
end
return self
end
--- Get all items that are NOT in both arrays.
-- @tparam unique_array|string|{string,...} other
-- @treturn @{unique_array} new
function M:diff(other)
other = unique_or_new(other)
local diff = M.new()
for _, v in ipairs(self) do
if not other[v] then
diff:add(v)
end
end
for _, v in ipairs(other) do
if not self[v] then
diff:add(v)
end
end
return diff
end
--- Get all items that are in both arrays.
-- @tparam unique_array|string|{string,...} other
-- @treturn @{unique_array} new
function M:intersects(other)
other = unique_or_new(other)
local intersection = M.new()
for _, v in ipairs(self) do
if other[v] then
intersection:add(v)
end
end
for _, v in ipairs(other) do
if self[v] and not intersection[v] then
intersection:add(v)
end
end
return intersection
end
--- Sort the unique_array in place.
-- @tparam[opt] function cmp Comparator @{sort} function to use
-- @treturn @{unique_array} self
function M:sort(cmp)
local class = getmetatable(self)
t_sort(self, cmp)
dictionary_sort(self, class)
return self
end
--- Create a new unique_array by concatenating together.
-- @tparam unique_array|string|{string,...} other
-- @treturn @{unique_array} new
function M:concat(other)
return M.new(self):add(other)
end
--- Find all members in a unique array that match the pattern.
-- @tparam string pattern Lua @{pattern}
-- @treturn @{unique_array} new unique array containing all elements that match.
function M:find(pattern)
local matches = M.new()
for _, value in ipairs(self) do
if value:find(pattern) then
matches:add(value)
end
end
return matches
end
--- Clear the array returning an empty array object
-- @treturn @{unique_array} self
function M:clear()
local class = getmetatable(self)
for i = #self, 1, -1 do
self[i] = nil
end
class.dictionary = {}
class.len = 0
return self
end
--- Functions
-- @section Functions
--- Does this array have all of the passed strings.
-- @tparam unique_array|string|{string,...} other
-- @treturn boolean every passed string is in the array
function M:all(other)
local params = type(other) == 'table' and other or { other }
local count = 0
for _, param in ipairs(params) do
if self[param] then
count = count + 1
end
end
return count == #params
end
M.has = M.all
--- Does this array have any of the passed strings.
-- @tparam unique_array|string|{string,...} other
-- @treturn boolean any passed string is in the array
function M:any(other)
for _, param in ipairs(type(other) == 'table' and other or { other }) do
if self[param] then
return true
end
end
return false
end
--- Does this array have none of the passed strings.
-- @tparam unique_array|string|{string,...} other
-- @treturn boolean no passed strings are in the array
function M:none(other)
for _, param in ipairs(type(other) == 'table' and other or { other }) do
if self[param] then
return false
end
end
return true
end
--- Do both arrays contain the same items
-- @tparam unique_array|string|{string,...} other
-- @treturn boolean
function M:same(other)
other = unique_or_new(other)
if #self == #other then
for _, value in ipairs(self) do
if not other[value] then
return false
end
end
return true
end
return false
end
--- Do the unique arrays have no items in common
-- @tparam unique_array|string|{string,...} other
-- @treturn boolean
function M:disjointed(other)
return #self:intersects(other) == 0
end
--- Convert the array to a string.
-- @treturn string
function M:tostring()
return table.concat(self, ', ')
end
--- Return a dictionary mapping of the array.
-- can be passed a function to set the value of the field.
-- @tparam[opt] function func value, index are passed as the first two paramaters
-- @tparam[opt] any ... additional values to pass to the function
-- @treturn dictionary
function M:make_dictionary(func, ...)
local dict = {}
for index, value in ipairs(self) do
dict[value] = func and func(value, index, ...) or index
end
return dict
end
--- Exports
-- @section Exports
local function from_dictionary(dict)
local array = {}
local i = 0
for k in pairs(dict) do
i = i + 1
array[i] = k
end
return create_class(array)
end
--- These functions are available when requiring this class.
-- @table exports
local exports = {
new = M.new, -- @{unique_array.new}
set = create_class, -- set the class on an existing table
dictionary = M.make_dictionary, -- @{unique_array.make_dictionary}
from_dictionary = from_dictionary
}
setmetatable(
exports,
{
__call = function(_, ...)
return M.new(...)
end
}
)
return exports

View File

@@ -0,0 +1,441 @@
--- For playing with colors.
-- @module Utils.Color
-- @usage local Color = require('__stdlib__/stdlib/utils/color')
local Color = {
__class = 'Color',
__index = require('__stdlib__/stdlib/core')
}
setmetatable(Color, Color)
local metatable
local table = require('__stdlib__/stdlib/utils/table')
local math = require('__stdlib__/stdlib/utils/math')
local color_list = require('__stdlib__/stdlib/utils/defines/color_list')
--- @table color @{defines.color}
Color.color = require('__stdlib__/stdlib/utils/defines/color')
--- @table anticolor @{defines.anticolor}
Color.anticolor = require('__stdlib__/stdlib/utils/defines/anticolor')
--- @table lightcolor @{defines.lightcolor}
Color.lightcolor = require('__stdlib__/stdlib/utils/defines/lightcolor')
--- Color Constructors
-- @section Color Constructors
--- Create a new Color object.
-- it can be passed, A Color, a string color name, an array,
-- a list of float paramaters (RGB), a color dictionary, or hex
-- @param any
-- @treturn Color
function Color.new(...)
if (...) == Color then
return Color.new(select(2, ...))
else
local c_type = type((...) or nil)
if not ... then
-- from a hex code or word color string, "red"
return Color.white()
elseif c_type == 'string' then
if (...):find('%x%x%x%x%x%x$') then
return Color.from_hex(...)
else
return Color.from_string(...)
end
elseif c_type == 'number' then
return Color.from_params(...)
elseif c_type == 'table' then
return Color.copy(...)
end
end
end
Color.__call = Color.new
--- Loads the color metatmethods into table without any checking.
-- @tparam Color color
-- @treturn color
function Color.load(color)
return setmetatable(color, metatable)
end
--- Copies the color into a new Color.
-- @tparam Color color
-- @tparam[opt] float alpha Change the alpha of the copied color
-- @treturn Color
function Color.copy(color, alpha)
if type(color) == 'table' then
if color == Color then
return Color.white()
elseif getmetatable(color) == metatable then
return setmetatable({ r = color.r, g = color.g, b = color.b, a = alpha or color.a or 0.5 }, metatable)
elseif type((next(color))) == 'number' then
return Color.from_array(color, alpha)
else
return Color.from_table(color, alpha)
end
else
Color.new(color, alpha)
end
end
--- Returns a white Color.
-- @treturn Color
function Color.white()
local color = { r = 1, g = 1, b = 1, a = 0.5 }
return setmetatable(color, metatable)
end
--- Returns a black color.
-- @treturn Color
function Color.black()
local color = { r = 0, g = 0, b = 0, a = 0.5 }
return setmetatable(color, metatable)
end
--- Returns a color from a string name if known.
-- Returns white if color string is unknown
-- @tparam string string_name
-- @tparam[opt] float alpha
-- @treturn Color
function Color.from_string(string_name, alpha)
local color = Color.color[string_name]
if color then
color.a = alpha or color.a or 0.5
return setmetatable(color, metatable)
end
return Color.white()
end
--- Converts a color in the rgb format to a color table.
-- @tparam[opt=0] float r 0-255 red
-- @tparam[opt=0] float g 0-255 green
-- @tparam[opt=0] float b 0-255 blue
-- @tparam[opt=255] float a 0-255 alpha
-- @treturn Concepts.Color
function Color.from_params(r, g, b, a)
local new = Color.normalize { r = r, g = g or r, b = b or r, a = a or 0.5 }
return setmetatable(new, metatable)
end
--- @see Color.from_params
Color.from_rgb = Color.from_params
--- Converts a color in the array format to a color in the table format.
-- @tparam array color the color to convert &mdash; { [1] = @{float}, [2] = @{float}, [3] = @{float}, [4] = @{float} }
-- @tparam[opt] float alpha
-- @treturn Concepts.Color a converted color &mdash; { r = c\_arr[1], g = c\_arr[2], b = c\_arr[3], a = c\_arr[4] }
function Color.from_array(color, alpha)
return Color.from_params(color[1] or 0, color[2] or 0, color[3] or 0, alpha or color[4] or 0.5)
end
--- Converts a color in the dictionary format to a color in the Color format.
-- @tparam table color the color to convert
-- @tparam[opt] float alpha
-- @treturn Color
function Color.from_table(color, alpha)
return Color.from_params(color.r or 0, color.g or 0, color.b or 0, alpha or color.a or 0.5)
end
--- Get a color table with a hexadecimal string.
-- Optionally provide the value for the alpha channel.
-- @tparam string color hexadecimal color string (#ffffff, not #fff)
-- @tparam[opt=1] float alpha the alpha value to set; such that *** 0 &#8924; value &#8924; 1 ***
-- @treturn Concepts.Color a color table with RGB converted from Hex and with alpha
function Color.from_hex(color, alpha)
assert(type(color) == 'string', 'missing color hex value')
local match = color:match('%x?%x?%x%x%x%x%x%x$')
color = tonumber(match, 16)
local new = { r = 0, g = 0, b = 0, a = 1 }
if #match == 8 then
new.r = bit32.extract(color, 24, 8) / 255
new.g = bit32.extract(color, 16, 8) / 255
new.b = bit32.extract(color, 8, 8) / 255
new.a = bit32.extract(color, 0, 8) / 255
elseif #match == 6 then
new.r = bit32.extract(color, 16, 8) / 255
new.g = bit32.extract(color, 8, 8) / 255
new.b = bit32.extract(color, 0, 8) / 255
new.a = alpha and (alpha > 1 and math.min(alpha, 255) / 255) or alpha or 0.5
end
return setmetatable(new, metatable)
end
--- @section end
--- Color Methods
-- @section Color Methods
---@deprecated
function Color.set(color, alpha)
color = color or { r = 1, g = 1, b = 1 }
if #color > 0 then
color = Color.from_array(color)
end
color.a = alpha or color.a or 1
return color
end
--- Normalizes a color between 0 and 1
-- @tparam Color color
-- @return Color
function Color.normalize(color)
color.a = color.a or 1
if color.r > 1 or color.g > 1 or color.b > 1 or color.a > 1 then
color.r = color.r > 1 and math.min(color.r, 255) / 255 or color.r
color.g = color.g > 1 and math.min(color.g, 255) / 255 or color.g
color.b = color.b > 1 and math.min(color.b, 255) / 255 or color.b
color.a = color.a > 1 and math.min(color.a, 255) / 255 or color.a
end
return color
end
--- Set the alpha channel on a color
-- @tparam Color color
-- @tparam[opt = 1] float alpha
-- @treturn Color
function Color.alpha(color, alpha)
alpha = alpha or 1
alpha = alpha > 1 and alpha / 255 or alpha
color.a = alpha
return color
end
--- Premultiply alpha
-- @tparam Color color
-- @tparam float alpha
-- @return Color
function Color.premul_alpha(color, alpha)
alpha = alpha > 1 and math.min(alpha, 255) / 255 or alpha
local new = {}
new.r = math.clamp(color.r * alpha)
new.g = math.clamp(color.g * alpha)
new.b = math.clamp(color.b * alpha)
new.a = color.a or 1
return setmetatable(new, metatable)
end
local function make_color(lhs, rhs)
if not Color.is_color(rhs) then
rhs = Color.new(rhs)
elseif not Color.is_color(lhs) then
lhs = Color.new(lhs)
end
return lhs, rhs
end
local function clamped(r, g, b, a)
local new = {}
new.r = math.clamp(r)
new.g = math.clamp(g)
new.b = math.clamp(b)
new.a = math.clamp(a)
return setmetatable(new, metatable)
end
--- Add 2 colors together.
-- @tparam Color lhs
-- @tparam Color rhs
-- @return Color
function Color.add(lhs, rhs)
lhs, rhs = make_color(lhs, rhs)
return clamped(lhs.r + rhs.r, lhs.g + rhs.g, lhs.b + rhs.b, math.max(lhs.a, rhs.a))
end
--- Subtract 2 colors together.
-- @tparam Color lhs
-- @tparam Color rhs
-- @return Color
function Color.subtract(lhs, rhs)
lhs, rhs = make_color(lhs, rhs)
return clamped(lhs.r - rhs.r, lhs.g - rhs.g, lhs.b - rhs.b, math.max(lhs.a, rhs.a))
end
--- Multiply 2 colors together.
-- @tparam Color lhs
-- @tparam Color rhs
-- @return Color
function Color.multiply(lhs, rhs)
lhs, rhs = make_color(lhs, rhs)
return clamped(lhs.r * rhs.r, lhs.g * rhs.g, lhs.b * rhs.b, math.max(lhs.a, rhs.a))
end
--- Add 2 colors together.
-- @tparam Color lhs
-- @tparam Color rhs
-- @return Color
function Color.divide(lhs, rhs)
lhs, rhs = make_color(lhs, rhs)
return clamped(lhs.r / rhs.r, lhs.g / rhs.g, lhs.b / rhs.b, math.max(lhs.a, rhs.a))
end
--- Modulo of 2 colors.
-- @tparam Color lhs
-- @tparam Color rhs
-- @treturn Color
function Color.modulo(lhs, rhs)
lhs, rhs = make_color(lhs, rhs)
return clamped(lhs.r % rhs.r, lhs.g % rhs.g, lhs.b % rhs.b, math.max(lhs.a, rhs.a))
end
--- Flip a color to white or black.
-- @tparam Color color
-- @treturn Color
function Color.unary(color)
return Color.len(color) < 1.5 and Color.white() or Color.black()
end
--- @section end
--- Color Functions
-- @section Color Functions
--- Get the length of a color by adding all its values together
-- @tparam Color color
-- @treturn number
function Color.len(color)
return color.r + color.g + color.b
end
--- Are both colors equal.
-- @tparam Color lhs
-- @tparam Color rhs
-- @treturn boolean
function Color.equals(lhs, rhs)
return lhs.r == rhs.r and lhs.g == rhs.g and lhs.b == rhs.b and lhs.a == rhs.a
end
--- Is LHS less than RHS.
-- @tparam Color lhs
-- @tparam Color rhs
-- @treturn boolean
function Color.less_than(lhs, rhs)
lhs, rhs = make_color(lhs, rhs)
return Color.len(lhs) < Color.len(rhs)
end
--- Is LHS less than or equal to RHS.
-- @tparam Color lhs
-- @tparam Color rhs
-- @treturn boolean
function Color.less_than_eq(lhs, rhs)
lhs, rhs = make_color(lhs, rhs)
return Color.len(lhs) <= Color.len(rhs)
end
--- Return a hex formated string from a color.
-- @tparam Color color
-- @treturn string
function Color.to_hex(color)
local str = {
string.format('%x', color.r * 255),
string.format('%x', color.g * 255),
string.format('%x', color.b * 255),
string.format('%x', (color.a or 1) * 255)
}
return '#' .. table.concat(str, '')
end
--- Return an array with 4 paramaters.
-- @tparam Color color
-- @treturn array
function Color.to_array(color)
return { color.r, color.g, color.b, color.a }
end
--- @see Color.to_array
Color.pack = Color.to_array
--- Return the color as 4 paramaters.
-- @tparam Color color
-- @return float
-- @return float
-- @return float
-- @return float
function Color.to_params(color)
return color.r, color.g, color.b, color.a
end
--- @see Color.to_params
Color.unpack = Color.to_params
--- Return the Color as a string.
-- @tparam Color color
-- @treturn string
function Color.to_string(color)
local str = {
'r = ' .. (color.r or 0),
'g = ' .. (color.g or 0),
'b = ' .. (color.b or 0),
'a = ' .. (color.a or 1)
}
return '{' .. table.concat(str, ', ') .. '}'
end
--- Is this a correctly formatted color.
-- @tparam Color color
-- @treturn boolean
function Color.is_complex(color)
local r, g, b, a
if type(color) == 'table' then
r = color.r and color.r <= 1
g = color.g and color.g <= 1
b = color.b and color.b <= 1
a = color.b and color.a <= 1
end
return r and b and g and a
end
--- Is this a Color object.
-- @tparam Color color
-- @treturn boolean
function Color.is_Color(color)
return getmetatable(color) == metatable
end
--- Is this a Color object or correctly formatted color table.
-- @tparam Color color
-- @treturn boolean
function Color.is_color(color)
return Color.is_Color(color) or Color.is_complex(color)
end
local function get_color_distance(lhs, rhs)
local dist = { r = lhs.r - rhs.r, g = lhs.g - rhs.g, b = lhs.b - rhs.b }
return (dist.r * dist.r + dist.g * dist.g + dist.b * dist.b)
end
--Takes a color and table of colors, finds key of color in table that most closely matches given color
function Color.best_color_match(color)
local closest
local min = 1
for color_name, compare in pairs(color_list) do
local distance = get_color_distance(color, compare)
min = (distance < min) and distance or min
if distance == min then
closest = color_name
end
end
return closest
end
--- @section end
metatable = {
__class = 'color',
__index = Color,
__call = Color.copy,
__add = Color.add,
__sub = Color.subtract,
__mul = Color.multiply,
__div = Color.divide,
__mod = Color.modulo,
__unm = Color.unary,
__eq = Color.equals,
__lt = Color.less_than,
__le = Color.less_than_eq,
__len = Color.len,
__tostring = Color.to_string,
__concat = _ENV.concat
}
return Color

View File

@@ -0,0 +1,74 @@
--- A defines module for retrieving colors by name.
-- Extends the Factorio defines table.
-- @usage require('__stdlib__/stdlib/utils/defines/anticolor')
-- @module defines.anticolor
-- @see Concepts.Color
--- Returns white for dark colors or black for lighter colors.
-- @table anticolor
-- @tfield Concepts.Color green defines.color.black
-- @tfield Concepts.Color grey defines.color.black
-- @tfield Concepts.Color lightblue defines.color.black
-- @tfield Concepts.Color lightgreen defines.color.black
-- @tfield Concepts.Color lightgrey defines.color.black
-- @tfield Concepts.Color lightred defines.color.black
-- @tfield Concepts.Color orange defines.color.black
-- @tfield Concepts.Color white defines.color.black
-- @tfield Concepts.Color yellow defines.color.black
-- @tfield Concepts.Color black defines.color.white
-- @tfield Concepts.Color blue defines.color.white
-- @tfield Concepts.Color brown defines.color.white
-- @tfield Concepts.Color darkblue defines.color.white
-- @tfield Concepts.Color darkgreen defines.color.white
-- @tfield Concepts.Color darkgrey defines.color.white
-- @tfield Concepts.Color darkred defines.color.white
-- @tfield Concepts.Color pink defines.color.white
-- @tfield Concepts.Color purple defines.color.white
-- @tfield Concepts.Color red defines.color.white
local anticolor = {}
local colors = require('__stdlib__/stdlib/utils/defines/color_list')
local anticolors = {
green = colors.black,
grey = colors.black,
lightblue = colors.black,
lightgreen = colors.black,
lightgrey = colors.black,
lightred = colors.black,
orange = colors.black,
white = colors.black,
yellow = colors.black,
black = colors.white,
blue = colors.white,
brown = colors.white,
darkblue = colors.white,
darkgreen = colors.white,
darkgrey = colors.white,
darkred = colors.white,
pink = colors.white,
purple = colors.white,
red = colors.white
}
local _mt = {
__index = function(_, c)
return anticolors[c] and { r = anticolors[c]['r'], g = anticolors[c]['g'], b = anticolors[c]['b'], a = anticolors[c]['a'] or 1 } or
{ r = 1, g = 1, b = 1, a = 1 }
end,
__pairs = function()
local k = nil
local c = anticolors
return function()
local v
k, v = next(c, k)
return k, (v and { r = v['r'], g = v['g'], b = v['b'], a = v['a'] or 1 }) or nil
end
end
}
setmetatable(anticolor, _mt)
_G.defines = _G.defines or {}
_G.defines.anticolor = anticolor
return anticolor

View File

@@ -0,0 +1,53 @@
--- A defines module for retrieving colors by name.
-- Extends the Factorio defines table.
-- @usage require('__stdlib__/stdlib/utils/defines/color')
-- @module defines.color
-- @see Concepts.Color
-- defines table is automatically required in all mod loading stages.
--- A table of colors allowing retrieval by color name.
-- @table color
-- @usage color = defines.color.red
-- @tfield Concepts.Color white
-- @tfield Concepts.Color black
-- @tfield Concepts.Color darkgrey
-- @tfield Concepts.Color grey
-- @tfield Concepts.Color lightgrey
-- @tfield Concepts.Color red
-- @tfield Concepts.Color darkred
-- @tfield Concepts.Color lightred
-- @tfield Concepts.Color green
-- @tfield Concepts.Color darkgreen
-- @tfield Concepts.Color lightgreen
-- @tfield Concepts.Color blue
-- @tfield Concepts.Color darkblue
-- @tfield Concepts.Color lightblue
-- @tfield Concepts.Color orange
-- @tfield Concepts.Color yellow
-- @tfield Concepts.Color pink
-- @tfield Concepts.Color purple
-- @tfield Concepts.Color brown
local color = {}
local colors = require('__stdlib__/stdlib/utils/defines/color_list')
local _mt = {
__index = function(_, c)
return colors[c] and { r = colors[c]['r'], g = colors[c]['g'], b = colors[c]['b'], a = colors[c]['a'] or 1 } or { r = 1, g = 1, b = 1, a = 1 }
end,
__pairs = function()
local k = nil
local c = colors
return function()
local v
k, v = next(c, k)
return k, (v and { r = v['r'], g = v['g'], b = v['b'], a = v['a'] or 1 }) or nil
end
end
}
setmetatable(color, _mt)
_G.defines = _G.defines or {}
_G.defines.color = color
return color

View File

@@ -0,0 +1,149 @@
return {
darkgray = { r = 0.6627, g = 0.6627, b = 0.6627, a = 1 },
aliceblue = { r = 0.9412, g = 0.9725, b = 1.0000, a = 1 },
antiquewhite = { r = 0.9804, g = 0.9216, b = 0.8431, a = 1 },
aqua = { r = 0.0000, g = 1.0000, b = 1.0000, a = 1 },
aquamarine = { r = 0.4980, g = 1.0000, b = 0.8314, a = 1 },
azure = { r = 0.9412, g = 1.0000, b = 1.0000, a = 1 },
beige = { r = 0.9608, g = 0.9608, b = 0.8627, a = 1 },
bisque = { r = 1.0000, g = 0.8941, b = 0.7686, a = 1 },
black = { r = 0.0000, g = 0.0000, b = 0.0000, a = 1 },
blanchedalmond = { r = 1.0000, g = 0.9216, b = 0.8039, a = 1 },
blue = { r = 0.0000, g = 0.0000, b = 1.0000, a = 1 },
blueviolet = { r = 0.5412, g = 0.1686, b = 0.8863, a = 1 },
brown = { r = 0.6471, g = 0.1647, b = 0.1647, a = 1 },
burlywood = { r = 0.8706, g = 0.7216, b = 0.5294, a = 1 },
cadetblue = { r = 0.3725, g = 0.6196, b = 0.6275, a = 1 },
chartreuse = { r = 0.4980, g = 1.0000, b = 0.0000, a = 1 },
chocolate = { r = 0.8235, g = 0.4118, b = 0.1176, a = 1 },
coral = { r = 1.0000, g = 0.4980, b = 0.3137, a = 1 },
cornflowerblue = { r = 0.3922, g = 0.5843, b = 0.9294, a = 1 },
cornsilk = { r = 1.0000, g = 0.9725, b = 0.8627, a = 1 },
crimson = { r = 0.8627, g = 0.0784, b = 0.2353, a = 1 },
cyan = { r = 0.0000, g = 1.0000, b = 1.0000, a = 1 },
darkblue = { r = 0.0000, g = 0.0000, b = 0.5451, a = 1 },
darkcyan = { r = 0.0000, g = 0.5451, b = 0.5451, a = 1 },
darkgoldenrod = { r = 0.7216, g = 0.5255, b = 0.0431, a = 1 },
darkgreen = { r = 0.0000, g = 0.3922, b = 0.0000, a = 1 },
darkgrey = { r = 0.6627, g = 0.6627, b = 0.6627, a = 1 },
darkkhaki = { r = 0.7412, g = 0.7176, b = 0.4196, a = 1 },
darkmagenta = { r = 0.5451, g = 0.0000, b = 0.5451, a = 1 },
darkolivegreen = { r = 0.3333, g = 0.4196, b = 0.1843, a = 1 },
darkorange = { r = 1.0000, g = 0.5490, b = 0.0000, a = 1 },
darkorchid = { r = 0.6000, g = 0.1961, b = 0.8000, a = 1 },
darkred = { r = 0.5451, g = 0.0000, b = 0.0000, a = 1 },
darksalmon = { r = 0.9137, g = 0.5882, b = 0.4784, a = 1 },
darkseagreen = { r = 0.5608, g = 0.7373, b = 0.5608, a = 1 },
darkslateblue = { r = 0.2824, g = 0.2392, b = 0.5451, a = 1 },
darkslategray = { r = 0.1843, g = 0.3098, b = 0.3098, a = 1 },
darkslategrey = { r = 0.1843, g = 0.3098, b = 0.3098, a = 1 },
darkturquoise = { r = 0.0000, g = 0.8078, b = 0.8196, a = 1 },
darkviolet = { r = 0.5804, g = 0.0000, b = 0.8275, a = 1 },
deeppink = { r = 1.0000, g = 0.0784, b = 0.5765, a = 1 },
deepskyblue = { r = 0.0000, g = 0.7490, b = 1.0000, a = 1 },
dimgray = { r = 0.4118, g = 0.4118, b = 0.4118, a = 1 },
dimgrey = { r = 0.4118, g = 0.4118, b = 0.4118, a = 1 },
dodgerblue = { r = 0.1176, g = 0.5647, b = 1.0000, a = 1 },
firebrick = { r = 0.6980, g = 0.1333, b = 0.1333, a = 1 },
floralwhite = { r = 1.0000, g = 0.9804, b = 0.9412, a = 1 },
forestgreen = { r = 0.1333, g = 0.5451, b = 0.1333, a = 1 },
fuchsia = { r = 1.0000, g = 0.0000, b = 1.0000, a = 1 },
gainsboro = { r = 0.8627, g = 0.8627, b = 0.8627, a = 1 },
ghostwhite = { r = 0.9725, g = 0.9725, b = 1.0000, a = 1 },
gold = { r = 1.0000, g = 0.8431, b = 0.0000, a = 1 },
goldenrod = { r = 0.8549, g = 0.6471, b = 0.1255, a = 1 },
gray = { r = 0.5020, g = 0.5020, b = 0.5020, a = 1 },
green = { r = 0.0000, g = 1.0000, b = 0.0000, a = 1 },
greenyellow = { r = 0.6784, g = 1.0000, b = 0.1843, a = 1 },
grey = { r = 0.5020, g = 0.5020, b = 0.5020, a = 1 },
honeydew = { r = 0.9412, g = 1.0000, b = 0.9412, a = 1 },
hotpink = { r = 1.0000, g = 0.4118, b = 0.7059, a = 1 },
indianred = { r = 0.8039, g = 0.3608, b = 0.3608, a = 1 },
indigo = { r = 0.2941, g = 0.0000, b = 0.5098, a = 1 },
ivory = { r = 1.0000, g = 1.0000, b = 0.9412, a = 1 },
khaki = { r = 0.9412, g = 0.9020, b = 0.5490, a = 1 },
lavender = { r = 0.9020, g = 0.9020, b = 0.9804, a = 1 },
lavenderblush = { r = 1.0000, g = 0.9412, b = 0.9608, a = 1 },
lawngreen = { r = 0.4863, g = 0.9882, b = 0.0000, a = 1 },
lemonchiffon = { r = 1.0000, g = 0.9804, b = 0.8039, a = 1 },
lightblue = { r = 0.6784, g = 0.8471, b = 0.9020, a = 1 },
lightcoral = { r = 0.9412, g = 0.5020, b = 0.5020, a = 1 },
lightcyan = { r = 0.8784, g = 1.0000, b = 1.0000, a = 1 },
lightgoldenrodyellow = { r = 0.9804, g = 0.9804, b = 0.8235, a = 1 },
lightgray = { r = 0.8275, g = 0.8275, b = 0.8275, a = 1 },
lightgreen = { r = 0.5647, g = 0.9333, b = 0.5647, a = 1 },
lightgrey = { r = 0.8275, g = 0.8275, b = 0.8275, a = 1 },
lightpink = { r = 1.0000, g = 0.7137, b = 0.7569, a = 1 },
lightsalmon = { r = 1.0000, g = 0.6275, b = 0.4784, a = 1 },
lightseagreen = { r = 0.1255, g = 0.6980, b = 0.6667, a = 1 },
lightskyblue = { r = 0.5294, g = 0.8078, b = 0.9804, a = 1 },
lightslategray = { r = 0.4667, g = 0.5333, b = 0.6000, a = 1 },
lightslategrey = { r = 0.4667, g = 0.5333, b = 0.6000, a = 1 },
lightsteelblue = { r = 0.6902, g = 0.7686, b = 0.8706, a = 1 },
lightyellow = { r = 1.0000, g = 1.0000, b = 0.8784, a = 1 },
lime = { r = 0.0000, g = 0.5020, b = 0.0000, a = 1 },
limegreen = { r = 0.1961, g = 0.8039, b = 0.1961, a = 1 },
linen = { r = 0.9804, g = 0.9412, b = 0.9020, a = 1 },
magenta = { r = 1.0000, g = 0.0000, b = 1.0000, a = 1 },
maroon = { r = 0.5020, g = 0.0000, b = 0.0000, a = 1 },
mediumaquamarine = { r = 0.4000, g = 0.8039, b = 0.6667, a = 1 },
mediumblue = { r = 0.0000, g = 0.0000, b = 0.8039, a = 1 },
mediumorchid = { r = 0.7294, g = 0.3333, b = 0.8275, a = 1 },
mediumpurple = { r = 0.5765, g = 0.4392, b = 0.8588, a = 1 },
mediumseagreen = { r = 0.2353, g = 0.7020, b = 0.4431, a = 1 },
mediumslateblue = { r = 0.4824, g = 0.4078, b = 0.9333, a = 1 },
mediumspringgreen = { r = 0.0000, g = 0.9804, b = 0.6039, a = 1 },
mediumturquoise = { r = 0.2824, g = 0.8196, b = 0.8000, a = 1 },
mediumvioletred = { r = 0.7804, g = 0.0824, b = 0.5216, a = 1 },
midnightblue = { r = 0.0980, g = 0.0980, b = 0.4392, a = 1 },
mintcream = { r = 0.9608, g = 1.0000, b = 0.9804, a = 1 },
mistyrose = { r = 1.0000, g = 0.8941, b = 0.8824, a = 1 },
moccasin = { r = 1.0000, g = 0.8941, b = 0.7098, a = 1 },
navajowhite = { r = 1.0000, g = 0.8706, b = 0.6784, a = 1 },
navy = { r = 0.0000, g = 0.0000, b = 0.5020, a = 1 },
oldlace = { r = 0.9922, g = 0.9608, b = 0.9020, a = 1 },
olive = { r = 0.5020, g = 0.5020, b = 0.0000, a = 1 },
olivedrab = { r = 0.4196, g = 0.5569, b = 0.1373, a = 1 },
orange = { r = 1.0000, g = 0.6471, b = 0.0000, a = 1 },
orangered = { r = 1.0000, g = 0.2706, b = 0.0000, a = 1 },
orchid = { r = 0.8549, g = 0.4392, b = 0.8392, a = 1 },
palegoldenrod = { r = 0.9333, g = 0.9098, b = 0.6667, a = 1 },
palegreen = { r = 0.5961, g = 0.9843, b = 0.5961, a = 1 },
paleturquoise = { r = 0.6863, g = 0.9333, b = 0.9333, a = 1 },
palevioletred = { r = 0.8588, g = 0.4392, b = 0.5765, a = 1 },
papayawhip = { r = 1.0000, g = 0.9373, b = 0.8353, a = 1 },
peachpuff = { r = 1.0000, g = 0.8549, b = 0.7255, a = 1 },
peru = { r = 0.8039, g = 0.5216, b = 0.2471, a = 1 },
pink = { r = 1.0000, g = 0.7529, b = 0.7961, a = 1 },
plum = { r = 0.8667, g = 0.6275, b = 0.8667, a = 1 },
powderblue = { r = 0.6902, g = 0.8784, b = 0.9020, a = 1 },
purple = { r = 0.5020, g = 0.0000, b = 0.5020, a = 1 },
red = { r = 1.0000, g = 0.0000, b = 0.0000, a = 1 },
rosybrown = { r = 0.7373, g = 0.5608, b = 0.5608, a = 1 },
royalblue = { r = 0.2549, g = 0.4118, b = 0.8824, a = 1 },
saddlebrown = { r = 0.5451, g = 0.2706, b = 0.0745, a = 1 },
salmon = { r = 0.9804, g = 0.5020, b = 0.4471, a = 1 },
sandybrown = { r = 0.9569, g = 0.6431, b = 0.3765, a = 1 },
seagreen = { r = 0.1804, g = 0.5451, b = 0.3412, a = 1 },
seashell = { r = 1.0000, g = 0.9608, b = 0.9333, a = 1 },
sienna = { r = 0.6275, g = 0.3216, b = 0.1765, a = 1 },
silver = { r = 0.7529, g = 0.7529, b = 0.7529, a = 1 },
skyblue = { r = 0.5294, g = 0.8078, b = 0.9216, a = 1 },
slateblue = { r = 0.4157, g = 0.3529, b = 0.8039, a = 1 },
slategray = { r = 0.4392, g = 0.5020, b = 0.5647, a = 1 },
slategrey = { r = 0.4392, g = 0.5020, b = 0.5647, a = 1 },
snow = { r = 1.0000, g = 0.9804, b = 0.9804, a = 1 },
springgreen = { r = 0.0000, g = 1.0000, b = 0.4980, a = 1 },
steelblue = { r = 0.2745, g = 0.5098, b = 0.7059, a = 1 },
tan = { r = 0.8235, g = 0.7059, b = 0.5490, a = 1 },
teal = { r = 0.0000, g = 0.5020, b = 0.5020, a = 1 },
thistle = { r = 0.8471, g = 0.7490, b = 0.8471, a = 1 },
tomato = { r = 1.0000, g = 0.3882, b = 0.2784, a = 1 },
turquoise = { r = 0.2510, g = 0.8784, b = 0.8157, a = 1 },
violet = { r = 0.9333, g = 0.5098, b = 0.9333, a = 1 },
wheat = { r = 0.9608, g = 0.8706, b = 0.7020, a = 1 },
white = { r = 1.0000, g = 1.0000, b = 1.0000, a = 1 },
whitesmoke = { r = 0.9608, g = 0.9608, b = 0.9608, a = 1 },
yellow = { r = 1.0000, g = 1.0000, b = 0.0000, a = 1 },
yellowgreen = { r = 0.6039, g = 0.8039, b = 0.1961, a = 1 }
}

View File

@@ -0,0 +1,55 @@
--- A defines module for retrieving colors by name.
-- Extends the Factorio defines table.
-- @usage require('__stdlib__/stdlib/utils/defines/lightcolor')
-- @module defines.lightcolor
-- @see Concepts.Color
-- defines table is automatically required in all mod loading stages.
--- Returns a lighter color of a named color.
-- @table lightcolor
-- @tfield Concepts.Color white defines.color.lightgrey
-- @tfield Concepts.Color grey defines.color.darkgrey
-- @tfield Concepts.Color lightgrey defines.color.grey
-- @tfield Concepts.Color red defines.color.lightred
-- @tfield Concepts.Color green defines.color.lightgreen
-- @tfield Concepts.Color blue defines.color.lightblue
-- @tfield Concepts.Color yellow defines.color.orange
-- @tfield Concepts.Color pink defines.color.purple
local lightcolor = {}
local colors = require('__stdlib__/stdlib/utils/defines/color_list')
local lightcolors = {
white = colors.lightgrey,
grey = colors.darkgrey,
lightgrey = colors.grey,
red = colors.lightred,
green = colors.lightgreen,
blue = colors.lightblue,
yellow = colors.orange,
pink = colors.purple
}
local _mt = {
{
__index = function(_, c)
return lightcolors[c] and { r = lightcolors[c]['r'], g = lightcolors[c]['g'], b = lightcolors[c]['b'], a = lightcolors[c]['a'] or 1 } or
{ r = 1, g = 1, b = 1, a = 1 }
end,
__pairs = function()
local k = nil
local c = lightcolors
return function()
local v
k, v = next(c, k)
return k, (v and { r = v['r'], g = v['g'], b = v['b'], a = v['a'] or 1 }) or nil
end
end
}
}
setmetatable(lightcolor, _mt)
_G.defines = _G.defines or {}
_G.defines.lightcolor = lightcolor
return lightcolor

View File

@@ -0,0 +1,31 @@
--- A defines module for retrieving the number of ticks in 1 unit of time.
-- Extends the Factorio defines table.
-- @module defines.time
-- defines table is automatically required in all mod loading stages.
local SECOND = 60
local MINUTE = SECOND * 60
local HOUR = MINUTE * 60
local DAY = HOUR * 24
local WEEK = DAY * 7
local MONTH = DAY * 30
local YEAR = DAY * 365
--- Returns the number of ticks in a second, minute, hour, day, week, month, or year.
-- @table time
-- @usage local ten_seconds = defines.time.second * 10
local time = {
second = SECOND, -- the number of Factorio ticks in a second
minute = MINUTE, -- the number of Factorio ticks in a second
hour = HOUR, -- the number of Factorio ticks in an hour
day = DAY, -- the number of Factorio ticks in an day
week = WEEK, -- the number of Factorio ticks in a week
month = MONTH, -- the number of Factorio ticks in a month (30 days)
year = YEAR -- the number of Factorio ticks in a year (365 days)
}
_G.defines = _G.defines or {}
_G.defines.time = time
return time

View File

@@ -0,0 +1,202 @@
--- Additional lua globals
-- @module Utils.Globals
_ENV = _ENV or _G
local config = require('__stdlib__/stdlib/config')
local Table = require('__stdlib__/stdlib/utils/table')
local Math = require('__stdlib__/stdlib/utils/math')
local String = require('__stdlib__/stdlib/utils/string')
local STDLIB = {
Math = Math,
String = String,
Table = Table
}
rawset(_ENV, 'STDLIB', STDLIB)
--Since debug can be overridden we define a fallback function here.
local ignored = {
data_traceback = true,
log_trace = true
}
local traceback = type(debug) == 'table' and debug.traceback or function()
return ''
end
rawset(_ENV, 'traceback', traceback)
local data_traceback = type(debug) == 'table' and debug.getinfo and function()
local str = {}
local level = 1
while true do
local trace = debug.getinfo(level)
if trace then
level = level + 1
if (trace.what == 'Lua' or trace.what == 'main') and not ignored[trace.name] then
local cur = trace.source:gsub('.*__stdlib__', '__stdlib__'):gsub('.*/Factorio%-Stdlib', '__stdlib__')
cur = cur .. ':' .. (trace.currentline or '0') .. ' in ' .. (trace.name or '???')
str[#str + 1] = cur
end
if trace.what == 'main' then
break
end
else
break
end
end
return ' [' .. table.concat(str, ', ') .. ']'
end or function()
return ''
end
rawset(_ENV, 'data_traceback', data_traceback)
local inspect = require('__stdlib__/stdlib/vendor/inspect')
rawset(_ENV, 'inspect', inspect)
-- Defines Mutates
require('__stdlib__/stdlib/utils/defines/color')
require('__stdlib__/stdlib/utils/defines/anticolor')
require('__stdlib__/stdlib/utils/defines/lightcolor')
require('__stdlib__/stdlib/utils/defines/time')
--- Require a file that may not exist
-- @tparam string module path to the module
-- @tparam boolean suppress_all suppress all errors, not just file_not_found
-- @treturn mixed
local function prequire(module, suppress_all)
local ok, err = pcall(require, module)
if ok then
return err
elseif not suppress_all and not err:find('^module .* not found') then
error(err)
end
end
rawset(_ENV, 'prequire', prequire)
--- Temporarily removes __tostring handlers and calls tostring
-- @tparam mixed t object to call rawtostring on
-- @treturn string
local function rawtostring(t)
local m = getmetatable(t)
if m then
local f = m.__tostring
m.__tostring = nil
local s = tostring(t)
m.__tostring = f
return s
else
return tostring(t)
end
end
rawset(_ENV, 'rawtostring', rawtostring)
--- Returns t if the expression is true. f if false
-- @tparam mixed exp The expression to evaluate
-- @tparam mixed t the true return
-- @tparam mixed f the false return
-- @treturn boolean
local function inline_if(exp, t, f)
if exp then
return t
else
return f
end
end
rawset(_ENV, 'inline_if', inline_if)
local function concat(lhs, rhs)
return tostring(lhs) .. tostring(rhs)
end
rawset(_ENV, 'concat', concat)
--- install the Table library into global table
function STDLIB.install_table()
for k, v in pairs(Table) do
_G.table[k] = v
end
end
--- Install the Math library into global math
function STDLIB.install_math()
for k, v in pairs(Math) do
_G.math[k] = v
end
end
--- Install the string library into global string
function STDLIB.install_string()
for k, v in pairs(String) do
_G.string[k] = v
end
setmetatable(string, nil)
end
--- Install Math, String, Table into their global counterparts.
function STDLIB.install_global_utils()
STDLIB.install.math()
STDLIB.install.string()
STDLIB.install.table()
end
--- Reload a required file, NOT IMPLEMENTED
function STDLIB.reload_class()
end
--- load the stdlib into globals, by default it loads everything into an ALLCAPS name.
-- Alternatively you can pass a dictionary of `[global names] -> [require path]`.
-- @tparam[opt] table files
-- @usage
-- STDLIB.create_stdlib_globals()
function STDLIB.create_stdlib_globals(files)
files =
files or
{
GAME = 'stdlib/game',
AREA = 'stdlib/area/area',
POSITION = 'stdlib/area/position',
TILE = 'stdlib/area/tile',
SURFACE = 'stdlib/area/surface',
CHUNK = 'stdlib/area/chunk',
COLOR = 'stdlib/utils/color',
ENTITY = 'stdlib/entity/entity',
INVENTORY = 'stdlib/entity/inventory',
RESOURCE = 'stdlib/entity/resource',
CONFIG = 'stdlib/misc/config',
LOGGER = 'stdlib/misc/logger',
QUEUE = 'stdlib/misc/queue',
EVENT = 'stdlib/event/event',
GUI = 'stdlib/event/gui',
PLAYER = 'stdlib/event/player',
FORCE = 'stdlib/event/force',
TABLE = 'stdlib/utils/table',
STRING = 'stdlib/utils/string',
MATH = 'stdlib/utils/math'
}
for glob, path in pairs(files) do
rawset(_ENV, glob, require('__stdlib__/' .. (path:gsub('%.', '/')))) -- extra () required to emulate select(1)
end
end
function STDLIB.create_stdlib_data_globals(files)
files =
files or
{
RECIPE = 'stdlib/data/recipe',
ITEM = 'stdlib/data/item',
FLUID = 'stdlib/data/fluid',
ENTITY = 'stdlib/data/entity',
TECHNOLOGY = 'stdlib/data/technology',
CATEGORY = 'stdlib/data/category',
DATA = 'stdlib/data/data',
TABLE = 'stdlib/utils/table',
STRING = 'stdlib/utils/string',
MATH = 'stdlib/utils/math',
COLOR = 'stdlib/utils/color'
}
STDLIB.create_stdlib_globals(files)
end
return STDLIB

View File

@@ -0,0 +1,458 @@
--- Is expression library
-- @module Utils.Is
-- @usage
-- local Is = require('__stdlib__/stdlib/utils/is')
-- Is.True(true)
-- Is.Not.True(false)
-- Is.Assert.True(true)
-- Is.Assert.Not.True(false)
--- Is Table
-- @section Table
--- Is the test true.
-- @table Is
-- @field Not Is the test not true.
-- @field Assert Assert that the test is true.
-- @field Assert.Not Assert that the test is not true.
--- Is Table Callers
-- @section Callers
--- Is the test truthy
-- @function Is
-- @tparam mixed var
-- @treturn boolean
local Is = {}
--- Is the test not truthy
-- @function Not
-- @tparam mixed var
-- @treturn boolean
Is.Not = {}
--- Assert that the test is Truthy
-- @function Assert
-- @tparam mixed var
-- @treturn boolean
Is.Assert = {}
--- Assert that the test is not Truthy
-- @function Assert.Not
-- @tparam mixed var
-- @treturn boolean
Is.Assert.Not = {}
--- Functions
-- @section Functions
local M = {}
local type = type
local floor = math.floor
local huge = math.huge
--- Returns the var if the passed variable is a table.
-- @tparam mixed var The variable to check
-- @treturn mixed
function M.Table(var)
return type(var) == 'table' and var
end
M.table = M.Table
--- Returns the var if the passed variable is a string.
-- @tparam mixed var The variable to check
-- @treturn mixed
function M.String(var)
return type(var) == 'string' and var
end
M.string = M.String
--- Returns the var if the passed variable is a number.
-- @tparam mixed var The variable to check
-- @treturn mixed
function M.Number(var)
return type(var) == 'number' and var
end
M.number = M.Number
function M.Thread(var)
return type(var) == 'thread' and var
end
M.thread = M.Thread
function M.Userdata(var)
return type(var) == 'userdata' and var
end
M.userdata = M.Userdata
--- Returns true if the passed variable is nil.
-- @tparam mixed var The variable to check
-- @treturn boolean
function M.Nil(var)
return type(var) == 'nil'
end
M.is_nil = M.Nil
--- Returns true if the passed variable is a boolean.
-- @tparam mixed var The variable to check
-- @treturn boolean
function M.Boolean(var)
return type(var) == 'boolean'
end
M.boolean = M.boolean
--- Returns true if the passed variable is true
-- @tparam mixed var The variable to check
-- @treturn boolean
function M.True(var)
return var == true
end
M.is_true = M.True
--- Returns the var if the passed variable is not nil or false.
-- @tparam mixed var The variable to check
-- @treturn mixed
function M.Truthy(var)
return var or false
end
M.truthy = M.Truthy
--- Returns true if the passed variable is false.
-- @tparam mixed var The variable to check
-- @treturn boolean
function M.False(var)
return var == false
end
M.is_false = M.False
--- Returns true if the passed variable is false or nil.
-- @tparam mixed var The variable to check
-- @treturn boolean
function M.Falsy(var)
return not var
end
M.falsy = M.Falsy
--- Returns true if the passed variable is nil, an empty table, or an empty string.
-- @tparam mixed var The variable to check
-- @treturn boolean
function M.Empty(var)
if M.Table(var) then
return table_size and table_size(var) == 0 or next(var) == nil
elseif M.String(var) then
return #string == 0
end
return M.Nil(var)
end
M.empty = M.Empty
function M.None(var)
return M.Empty(var) or M.False(var) or var == 0 or var ~= var
end
M.none = M.None
--- Returns the passed var if it is positive.
-- @tparam mixed var The variable to check
-- @treturn mixed
function M.Positive(var)
return M.Number(var) and var >= 0 and var
end
M.positive = M.Positive
--- Returns the passed var if it is odd.
-- @tparam mixed var The variable to check
-- @treturn mixed
function M.odd(var)
return M.number(var) and (var % 2 ~= 0) and var
end
--- Returns the passed var if it is even.
-- @tparam mixed var The variable to check
-- @treturn mixed
function M.even(var)
return M.number(var) and (var % 2 == 0) and var
end
--- Returns the passed var if it is negative.
-- @tparam mixed var The variable to check
-- @treturn mixed
function M.Negative(var)
return M.Number(var) and var < 0 and var
end
M.negative = M.Negative
--- Returns the passed var if it is not a number.
-- @tparam mixed var The variable to check
-- @treturn mixed
function M.NaN(var)
return var ~= var
end
M.nan = M.NaN
--- Returns the passed var if it is finite.
-- @tparam mixed var The variable to check
-- @treturn mixed
function M.Finite(var)
return M.Number(var) and (var < huge and var > -huge) and var
end
M.finite = M.Finite
--- Returns the passed var if it is an int.
-- @tparam mixed var The variable to check
-- @treturn mixed
function M.Int(var)
return M.Finite(var) and rawequal(floor(var), var) and var
end
M.int = M.Int
--- Returns the passed var if it is an int8.
-- @tparam mixed var The variable to check
-- @treturn mixed
function M.Int8(var)
return M.Int(var) and var >= -128 and var <= 127 and var
end
M.int8 = M.Int8
--- Returns the passed var if it is an int16.
-- @tparam mixed var The variable to check
-- @treturn mixed
function M.Int16(var)
return M.Int(var) and var >= -32768 and var <= 32767 and var
end
M.int16 = M.Int16
--- Returns the passed var if it is an int32.
-- @tparam mixed var The variable to check
-- @treturn mixed
function M.Int32(var)
return M.Int(var) and var >= -2147483648 and var <= 2147483647 and var
end
M.int32 = M.Int32
--- Returns the passed var if it is an unsigned int.
-- @tparam mixed var The variable to check
-- @treturn mixed
function M.Unsigned(var)
return Is.Number(var) and (var < huge and var >= 0) and var
end
M.unsigned = M.Unsigned
--- Returns the passed var if it is an unsigned int.
-- @tparam mixed var The variable to check
-- @treturn mixed
function M.UInt(var)
return M.Unsigned(var) and rawequal(floor(var), var) and var
end
M.uint = M.UInt
--- Returns the passed var if it is an unsigned int8.
-- @tparam mixed var The variable to check
-- @treturn mixed
function M.UInt8(var)
return M.UInt(var) and var <= 255 and var
end
M.uint8 = M.UInt8
--- Returns the passed var if it is an unsigned int16.
-- @tparam mixed var The variable to check
-- @treturn mixed
function M.UInt16(var)
return M.UInt(var) and var <= 65535 and var
end
M.uint16 = M.UInt16
--- Returns the passed var if it is an unsigned int32.
-- @tparam mixed var The variable to check
-- @treturn mixed
function M.UInt32(var)
return M.UInt(var) and var <= 4294967295 and var
end
M.uint32 = M.UInt32
--- Returns the passed var if it is a float.
-- @tparam mixed var The variable to check
-- @treturn mixed
function M.Float(var)
return M.number(var) and var >= 0 and var < 1 and var
end
M.float = M.Float
--- Returns the passed var if it is a full position.
-- @tparam mixed var The variable to check
-- @treturn mixed
function M.Position(var)
return M.Table(var) and (var.x and var.y) and var
end
M.position = M.Position
--- Returns the passed var if it is a full area.
-- @tparam mixed var The variable to check
-- @treturn mixed
function M.Area(var)
return M.Table(var) and (M.Position(var.left_top) and M.Position(var.right_bottom)) and var
end
M.area = M.Area
--- Returns the passed var if it is a simple position/vector.
-- @tparam mixed var The variable to check
-- @treturn mixed
function M.Vector(var)
return M.Table(var) and ((M.Number(var[1]) and M.Number(var[2])) or M.Position(var)) and var
end
M.vector = M.Vector
--- Returns the passed var if it is a simple area/boundingbox.
-- @tparam mixed var The variable to check
-- @treturn mixed
function M.BoundingBox(var)
return M.Table(var) and (M.Vector(var[1]) and M.Vector(var[2]))
end
M.boundingbox = M.BoundingBox
M.bounding_box = M.BoundingBox
M.Bounding_Box = M.BoundingBox
--- Returns the hex value of the passed var if it is hexadecimal.
-- @tparam mixed var The variable to check
-- @treturn mixed
function M.Hex(var)
return M.String(var) and var:match('%x%x%x%x%x%x$')
end
M.hex = M.Hex
--- Returns true if the passed variable is a single alphbetical word.
-- Does not allow any special chars
-- @tparam mixed var The variable to check
-- @treturn boolean true if the passed variable is a single alphbetical word
function M.StrictWord(var)
return M.String(var) and var:find('^[%a]+$') == 1
end
M.strict_word = M.StrictWord
--- Returns true if the passed variable is a single alphbetical word.
-- Allows _ and - as part of the word
-- @tparam mixed var The variable to check
-- @treturn boolean true if the passed variable is a single alphbetical word
function M.AlphabetWord(var)
return M.String(var) and var:find('^[%a%_%-]+$') == 1
end
M.Word = M.AlphabetWord
--- Returns true if the passed variable is a single alphbetical word.
-- Must start with a letter, allows _ and - as part of the word
-- @tparam mixed var The variable to check
-- @treturn boolean true if the passed variable is a single alphbetical word
function M.AlphanumWord(var)
return M.String(var) and var:find('^%a+[%w%_%-]*$') == 1
end
M.Alpha = M.AlphanumWord
M.alpha = M.AlphanumWord
M.alphanumword = M.AlphanumWord
--- Is this a factorio object
-- @tparam LuaObject var The variable to check
-- @treturn mixed the var if this is an LuaObject
function M.Object(var)
return M.Table(var) and var.__self and var
end
M.object = M.Object
--- Is this factorio object valid
-- @tparam LuaObject var The variable to check
-- @treturn mixed the var if this is a valid LuaObject
function M.Valid(var)
return M.Object(var) and var.valid and var
end
M.valid = M.Valid
--- Returns true if the passed variable is a callable function.
-- @tparam mixed var The variable to check
-- @treturn boolean true if the passed variable is a callable function
function M.Callable(var)
return type(var) == 'function' or type((getmetatable(var) or {}).__call) == 'function'
end
M.callable = M.Callable
M.Function = M.Callable
M.is_function = M.Callable
setmetatable(
Is,
{
__index = function(_, k)
return M[k] and function(_assert)
return M[k](_assert)
end or nil
end,
__call = function(_, ...)
return (...)
end
}
)
setmetatable(
Is.Not,
{
__index = function(_, k)
return M[k] and function(_assert)
return not M[k](_assert)
end or nil
end,
__call = function(_, ...)
return not (...)
end
}
)
Is.is_not = Is.Not
-- convenience function for un-lambda-ing deferred error messages
local function safe_invoke(f)
local ok, msg = xpcall(f, debug.traceback)
if not ok then
-- ensure msg really is a string so there is theoretically no chance
-- of a triple fault (i.e.: from a monkey-patched debug.traceback
-- returning something that now fails to concatenate to a string)
if type(msg) == 'string' then
msg = '<<< DOUBLE FAULT: ' .. msg .. ' >>>'
end
end
-- for sanity-preservation, always return something truthy
return msg or 'Unknown Error'
end
setmetatable(
Is.Assert,
{
__index = function(_, k)
return M[k] and function(_assert, _message, _level)
_level = tonumber(_level) or 3 ---@type integer
return M[k](_assert) or error(type(_message) == 'function' and safe_invoke(_message) or _message or 'assertion failed', _level)
end or nil
end,
__call = function(_, ...)
local param = { ... }
local _level = tonumber(param[3]) or 3 --[[@as integer]]
return param[1] or error(type(param[2]) == 'function' and safe_invoke(param[2]) or param[2] or 'assertion failed', _level)
end
}
)
Is.assert = Is.Assert
setmetatable(
Is.Assert.Not,
{
__index = function(_, k)
return M[k] and function(_assert, _message, _level)
_level = tonumber(_level) or 3 ---@type integer
return not M[k](_assert) or error(type(_message) == 'function' and safe_invoke(_message) or _message or 'assertion failed', _level)
end or nil
end,
__call = function(_, ...)
local param = { ... }
local _level = tonumber(param[3]) or 3 --[[@as integer]]
return not param[1] or error(type(param[2]) == 'function' and safe_invoke(param[2]) or param[2] or 'assertion failed', _level)
end
}
)
Is.assert.is_not = Is.Assert.Not
return Is

View File

@@ -0,0 +1,95 @@
--- Iterator library.
-- @module Utils.Iter
local Iter = {}
local pairs = pairs
local ipairs = ipairs
Iter.pairs = pairs
Iter.ipairs = ipairs
function Iter.spairs(t, order)
-- collect the keys
local keys = {}
for k in pairs(t) do
keys[#keys + 1] = k
end
-- if order function given, sort by it by passing the table and keys a, b,
-- otherwise just sort the keys
if order then
table.sort(
keys,
function(a, b)
return order(t, a, b)
end
)
else
table.sort(keys)
end
-- return the iterator function
local i = 0
return function()
i = i + 1
if keys[i] then
return keys[i], t[keys[i]]
end
end
end
function Iter.top(t, stop)
local start = #t
stop = stop or 1
return function()
if start >= stop and t[start] ~= nil then
local cur = start
start = start - 1
return cur, t[cur]
end
return nil
end
end
function Iter.wrap(t, start, reverse)
--[[
-- Returns an iterator that iterates over integer keys in table `t` from the specified start position, wrapping
-- around and ending when it reaches `start` again.
--
-- `dir` specifies the direction to iterate (negative values for reverse, otherwise forward)
-- `start` specifies the start location. If `nil`, the first returned item will be at the at the start of the table
-- (or the end of the table, if `dir` is negative)
--
-- Behavior if the table changes size during iteration is undefined.
]]
local dir = (reverse and -1) or 1
local len = #t
local i = start
start = start or (reverse and len) or 1
return function()
if i == nil then
i = start
return i, t[i]
end
i = i + dir
if i < 1 then
i = i + len
elseif i > len then
i = i - len
end
if i == start then
return nil
end
return i, t[i]
end
end
function Iter.tpairs(...)
return ipairs(type(...) == 'table' and ... or { ... })
end
return Iter

View File

@@ -0,0 +1,287 @@
--- Extends Lua 5.2 math.
-- @module Utils.math
-- @see math
-- @usage local math = require('__stdlib__/stdlib/utils/math')
local Math = {}
Math.frexp = math.frexp
Math.sqrt = math.sqrt
Math.asin = math.asin
Math.random = math.random
Math.huge = math.huge
Math.abs = math.abs
Math.ldexp = math.ldexp
Math.exp = math.exp
Math.pow = math.pow
Math.pi = math.pi
Math.tan = math.tan
Math.acos = math.acos
Math.ceil = math.ceil
Math.atan2 = math.atan2
Math.tanh = math.tanh
Math.sin = math.sin
Math.min = math.min
Math.deg = math.deg
Math.sinh = math.sinh
Math.rad = math.rad
Math.randomseed = math.randomseed
Math.cosh = math.cosh
Math.modf = math.modf
Math.cos = math.cos
Math.atan = math.atan
Math.max = math.max
Math.log = math.log
Math.fmod = math.fmod
Math.floor = math.floor
for k, v in pairs(math) do if not Math[k] then Math[k] = v end end
local math_abs = math.abs
local math_floor = math.floor
local math_ceil = math.ceil
local math_min = math.min
local math_max = math.max
local math_huge = math.huge
local math_pi = math.pi
local math_log = math.log
local unpack = table.unpack
-- (( Math Constants
Math.DEG2RAD = math_pi / 180
Math.RAD2DEG = 180 / math_pi
Math.EPSILON = 1.401298e-45
Math.MAXINT8 = 128
Math.MININT8 = -128
Math.MAXUINT8 = 255
Math.MAX_INT8 = Math.MAXINT8
Math.MIN_INT8 = Math.MININT8
Math.MAX_UINT8 = Math.MAXUINT8
Math.MAXINT16 = 32768
Math.MININT16 = -32768
Math.MAXUINT16 = 65535
Math.MAX_INT16 = Math.MAXINT16
Math.MIN_INT16 = Math.MININT16
Math.MAX_UINT16 = Math.MAXUINT16
Math.MAXINT = 2147483648
Math.MAX_INT = Math.MAXINT
Math.MAXINT32 = Math.MAXINT
Math.MAX_INT32 = Math.MAXINT
Math.MAXUINT = 4294967296
Math.MAX_UINT = Math.MAXUINT
Math.MAXUINT32 = Math.MAXUINT
Math.MAX_UINT32 = Math.MAXUINT
Math.MININT = -2147483648
Math.MIN_INT = Math.MININT
Math.MININT32 = Math.MININT
Math.MIN_INT32 = Math.MININT
Math.MAXINT64 = 9223372036854775808
Math.MININT64 = -9223372036854775808
Math.MAXUINT64 = 18446744073709551615
Math.MAX_INT64 = Math.MAXINT64
Math.MIN_INT64 = Math.MININT64
Math.MAX_UINT64 = Math.MAXUINT64
-- ))
local function tuple(...)
return type(...) == 'table' and ... or { ... }
end
function Math.log10(x)
return math_log(x, 10)
end
--- Round a number.
-- @tparam number x
-- @treturn number the rounded number
function Math.round(x)
return x >= 0 and math_floor(x + 0.5) or math_ceil(x - 0.5)
end
-- Returns the number x rounded to p decimal places.
-- @tparam number x
-- @tparam[opt=0] int p the number of decimal places to round to
-- @treturn number rounded to p decimal spaces.
function Math.round_to(x, p)
local e = 10 ^ (p or 0)
return math_floor(x * e + 0.5) / e
end
-- Returns the number floored to p decimal spaces.
-- @tparam number x
-- @tparam[opt=0] int p the number of decimal places to floor to
-- @treturn number floored to p decimal spaces.
function Math.floor_to(x, p)
if (p or 0) == 0 then return math_floor(x) end
local e = 10 ^ p
return math_floor(x * e) / e
end
-- Returns the number ceiled to p decimal spaces.
-- @tparam number x
-- @tparam[opt=0] int p the number of decimal places to ceil to
-- @treturn number ceiled to p decimal spaces.
function Math.ceil_to(x, p)
local e = 10 ^ (p or 0)
return math_ceil(x * e + 0.5) / e
end
-- Various average (means) algorithms implementation
-- See: http://en.wikipedia.org/wiki/Average
--- Calculates the sum of a sequence of values.
-- @tparam tuple ... a tuple of numbers
-- @treturn the sum
function Math.sum(...)
local x = tuple(...)
local s = 0
for _, v in ipairs(x) do s = s + v end
return s
end
--- Calculates the arithmetic mean of a set of values.
-- @tparam array x an array of numbers
-- @treturn number the arithmetic mean
function Math.arithmetic_mean(...)
local x = tuple(...)
return (Math.sum(x) / #x)
end
Math.avg = Math.arithmetic_mean
Math.average = Math.arithmetic_mean
--- Calculates the geometric mean of a set of values.
-- @tparam array x an array of numbers
-- @treturn number the geometric mean
function Math.geometric_mean(...)
local x = tuple(...)
local prod = 1
for _, v in ipairs(x) do prod = prod * v end
return (prod ^ (1 / #x))
end
--- Calculates the harmonic mean of a set of values.
-- @tparam tuple ... an array of numbers
-- @treturn number the harmonic mean
function Math.harmonic_mean(...)
local x = tuple(...)
local s = 0
for _, v in ipairs(x) do s = s + (1 / v) end
return (#x / s)
end
--- Calculates the quadratic mean of a set of values.
-- @tparam tuple ... an array of numbers
-- @treturn number the quadratic mean
function Math.quadratic_mean(...)
local x = tuple(...)
local squares = 0
for _, v in ipairs(x) do squares = squares + (v * v) end
return math.sqrt((1 / #x) * squares)
end
--- Calculates the generalized mean (to a specified power) of a set of values.
-- @tparam number p power
-- @tparam tuple ... an array of numbers
-- @treturn number the generalized mean
function Math.generalized_mean(p, ...)
local x = tuple(...)
local sump = 0
for _, v in ipairs(x) do sump = sump + (v ^ p) end
return ((1 / #x) * sump) ^ (1 / p)
end
--- Calculates the weighted mean of a set of values.
-- @tparam array x an array of numbers
-- @tparam array w an array of number weights for each value
-- @treturn number the weighted mean
function Math.weighted_mean(x, w)
local sump = 0
for i, v in ipairs(x) do sump = sump + (v * w[i]) end
return sump / Math.sum(w)
end
--- Calculates the midrange mean of a set of values.
-- @tparam array x an array of numbers
-- @treturn number the midrange mean
function Math.midrange_mean(...)
local x = tuple(...)
return 0.5 * (math_min(unpack(x)) + math_max(unpack(x)))
end
--- Calculates the energetic mean of a set of values.
-- @tparam array x an array of numbers
-- @treturn number the energetic mean
function Math.energetic_mean(...)
local x = tuple(...)
local s = 0
for _, v in ipairs(x) do s = s + (10 ^ (v / 10)) end
return 10 * Math.log10((1 / #x) * s)
end
--- Returns the number x clamped between the numbers min and max.
-- @tparam number x
-- @tparam[opt=0] number min
-- @tparam[opt=1] number max
-- @treturn number clamped between min and max
function Math.clamp(x, min, max)
min, max = min or 0, max or 1
return x < min and min or (x > max and max or x)
end
--- Linear interpolation or 2 numbers.
-- @tparam number a
-- @tparam number b
-- @tparam float amount
-- @treturn number
function Math.lerp(a, b, amount)
return a + (b - a) * Math.clamp(amount, 0, 1)
end
--- Smooth.
-- @tparam number a
-- @tparam number b
-- @tparam float amount
-- @treturn number
function Math.smooth(a, b, amount)
local t = Math.clamp(amount, 0, 1)
local m = t * t * (3 - 2 * t)
return a + (b - a) * m
end
--- Approximately the same
-- @tparam number a
-- @tparam number b
-- @treturn boolean
function Math.approximately(a, b)
return math_abs(b - a) < math_max(1e-6 * math_max(math_abs(a), math_abs(b)), 1.121039e-44)
end
--- Is x a number.
-- @tparam number x
-- @treturn boolean
function Math.is_number(x)
return x == x and x ~= math_huge
end
--- Is x an integer.
-- @tparam number x
-- @treturn boolean
function Math.is_integer(x)
return x == math_ceil(x)
end
--- Is x unsigned.
-- @tparam number x
-- @treturn boolean
function Math.is_unsigned(x)
return x >= 0
end
return Math

View File

@@ -0,0 +1,284 @@
--- Extends Lua 5.2 string.
-- @module Utils.string
-- @see string
-- @usage local string = require('__stdlib__/stdlib/utils/string')
local String = {}
String.find = string.find
String.lower = string.lower
String.gmatch = string.gmatch
String.sub = string.sub
String.byte = string.byte
String.char = string.char
String.reverse = string.reverse
String.dump = string.dump
String.rep = string.rep
String.format = string.format
String.match = string.match
String.gsub = string.gsub
String.len = string.len
String.upper = string.upper
getmetatable('').__index = String -- Allow string syntatic sugar to work with this class
for k, v in pairs(string) do if not String[k] then String[k] = v end end
local concat = table.concat
local insert = table.insert
local ceil = math.ceil
local abs = math.abs
--- Returns a copy of the string with any leading or trailing whitespace from the string removed.
-- @tparam string s the string to remove leading or trailing whitespace from
-- @treturn string a copy of the string without leading or trailing whitespace
function String.trim(s)
return (s:gsub([[^%s*(.-)%s*$]], '%1'))
end
--- Tests if a string starts with a given substring.
-- @tparam string s the string to check for the start substring
-- @tparam string start the substring to test for
-- @treturn boolean true if the start substring was found in the string
function String.starts_with(s, start)
return s:find(start, 1, true) == 1
end
--- Tests if a string ends with a given substring.
-- @tparam string s the string to check for the end substring
-- @tparam string ends the substring to test for
-- @treturn boolean true if the end substring was found in the string
function String.ends_with(s, ends)
return #s >= #ends and s:find(ends, #s - #ends + 1, true) and true or false
end
--- Tests if a string contains a given substring.
-- @tparam string s the string to check for the substring
-- @tparam string contains the substring to test for
-- @treturn boolean true if the substring was found in the string
function String.contains(s, contains)
return s and s:find(contains) ~= nil
end
--- Tests whether a string is empty.
-- @tparam string s the string to test
-- @treturn boolean true if the string is empty
function String.is_empty(s)
return s == nil or s == ''
end
--- does s only contain alphabetic characters?
-- @string s a string
function String.is_alpha(s)
return s:find('^%a+$') == 1
end
--- does s only contain digits?
-- @string s a string
function String.is_digit(s)
return s:find('^%d+$') == 1
end
--- does s only contain alphanumeric characters?
-- @string s a string
function String.is_alnum(s)
return s:find('^%w+$') == 1
end
--- does s only contain spaces?
-- @string s a string
function String.is_space(s)
return s:find('^%s+$') == 1
end
--- does s only contain lower case characters?
-- @string s a string
function String.is_lower(s)
return s:find('^[%l%s]+$') == 1
end
--- does s only contain upper case characters?
-- @string s a string
function String.is_upper(s)
return s:find('^[%u%s]+$') == 1
end
--- iniital word letters uppercase ('title case').
-- Here 'words' mean chunks of non-space characters.
-- @string s the string
-- @return a string with each word's first letter uppercase
function String.title(s)
return (s:gsub([[(%S)(%S*)]], function(f, r)
return f:upper() .. r:lower()
end))
end
local ellipsis = '...'
local n_ellipsis = #ellipsis
--- Return a shortened version of a string.
-- Fits string within w characters. Removed characters are marked with ellipsis.
-- @string s the string
-- @int w the maxinum size allowed
-- @bool tail true if we want to show the end of the string (head otherwise)
-- @usage ('1234567890'):shorten(8) == '12345...'
-- @usage ('1234567890'):shorten(8, true) == '...67890'
-- @usage ('1234567890'):shorten(20) == '1234567890'
function String.shorten(s, w, tail)
if #s > w then
if w < n_ellipsis then return ellipsis:sub(1, w) end
if tail then
local i = #s - w + 1 + n_ellipsis
return ellipsis .. s:sub(i)
else
return s:sub(1, w - n_ellipsis) .. ellipsis
end
end
return s
end
--- concatenate the strings using this string as a delimiter.
-- @string s the string
-- @param seq a table of strings or numbers
-- @usage (' '):join {1,2,3} == '1 2 3'
function String.join(s, seq)
return concat(seq, s)
end
local function _just(s, w, ch, left, right)
local n = #s
if w > n then
if not ch then ch = ' ' end
local f1, f2
if left and right then
local rn = ceil((w - n) / 2)
local ln = w - n - rn
f1 = ch:rep(ln)
f2 = ch:rep(rn)
elseif right then
f1 = ch:rep(w - n)
f2 = ''
else
f2 = ch:rep(w - n)
f1 = ''
end
return f1 .. s .. f2
else
return s
end
end
--- left-justify s with width w.
-- @string s the string
-- @int w width of justification
-- @string[opt=' '] ch padding character
function String.ljust(s, w, ch)
return _just(s, w, ch, true, false)
end
--- right-justify s with width w.
-- @string s the string
-- @int w width of justification
-- @string[opt=' '] ch padding character
function String.rjust(s, w, ch)
return _just(s, w, ch, false, true)
end
--- center-justify s with width w.
-- @string s the string
-- @int w width of justification
-- @string[opt=' '] ch padding character
function String.center(s, w, ch)
return _just(s, w, ch, true, true)
end
local noop = function(...)
return ...
end
--- Splits a string into an array.
-- Note: Empty split substrings are not included in the resulting table.
-- <p>For example, `string.split("foo.bar...", ".", false)` results in the table `{"foo", "bar"}`.
-- @tparam string s the string to split
-- @tparam[opt="."] string sep the separator to use.
-- @tparam[opt=false] boolean pattern whether to interpret the separator as a lua pattern or plaintext for the string split
-- @tparam[opt] function func pass each split string through this function.
-- @treturn {string,...} an array of strings
function String.split(s, sep, pattern, func)
sep = sep or '.'
sep = sep ~= '' and sep or '.'
sep = not pattern and sep:gsub('([^%w])', '%%%1') or sep
func = func or noop
local fields = {}
local start_idx, end_idx = s:find(sep)
local last_find = 1
while start_idx do
local substr = s:sub(last_find, start_idx - 1)
if substr:len() > 0 then table.insert(fields, func(s:sub(last_find, start_idx - 1))) end
last_find = end_idx + 1
start_idx, end_idx = s:find(sep, end_idx + 1)
end
local substr = s:sub(last_find)
if substr:len() > 0 then insert(fields, func(s:sub(last_find))) end
return fields
end
--- Return the ordinal suffix for a number.
-- @tparam number n
-- @tparam boolean prepend_number if the passed number should be pre-pended
-- @treturn string the ordinal suffix
function String.ordinal_suffix(n, prepend_number)
if tonumber(n) then
n = abs(n) % 100
local d = n % 10
if d == 1 and n ~= 11 then
return (prepend_number and n or '') .. 'st'
elseif d == 2 and n ~= 12 then
return (prepend_number and n or '') .. 'nd'
elseif d == 3 and n ~= 13 then
return (prepend_number and n or '') .. 'rd'
else
return (prepend_number and n or '') .. 'th'
end
end
return prepend_number and n
end
local exponent_multipliers = {
['y'] = 0.000000000000000000000001,
['z'] = 0.000000000000000000001,
['a'] = 0.000000000000000001,
['f'] = 0.000000000000001,
['p'] = 0.000000000001,
['n'] = 0.000000001,
['u'] = 0.000001,
['m'] = 0.001,
['c'] = 0.01,
['d'] = 0.1,
[' '] = 1,
['h'] = 100,
['k'] = 1000,
['M'] = 1000000,
['G'] = 1000000000,
['T'] = 1000000000000,
['P'] = 1000000000000000,
['E'] = 1000000000000000000,
['Z'] = 1000000000000000000000,
['Y'] = 1000000000000000000000000
}
--- Convert a metric string prefix to a number value.
-- @tparam string str
-- @treturn float
function String.exponent_number(str)
if type(str) == 'string' then
local value, exp = str:match('([%-+]?[0-9]*%.?[0-9]+)([yzafpnumcdhkMGTPEZY]?)') ---@diagnostic disable-line: spell-check
exp = exp or ' '
value = (value or 0) * (exponent_multipliers[exp] or 1)
return value
elseif type(str) == 'number' then
return str
end
return 0
end
return String

View File

@@ -0,0 +1,574 @@
--- Extends Lua 5.2 table.
-- @module Utils.table
-- @see table
-- @usage local table = require('__stdlib__/stdlib/utils/table')
local Table = {}
Table.remove = table.remove
Table.sort = table.sort
Table.pack = table.pack
Table.unpack = table.unpack
Table.insert = table.insert
Table.concat = table.concat
-- Import base lua table into Table
for k, v in pairs(table) do if not Table[k] then Table[k] = v end end
--- Given a mapping function, creates a transformed copy of the table
-- by calling the function for each element in the table, and using
-- the result as the new value for the key. Passes the index as second argument to the function.
-- @usage a= { 1, 2, 3, 4, 5}
-- table.map(a, function(v) return v * 10 end) --produces: { 10, 20, 30, 40, 50 }
-- @usage a = {1, 2, 3, 4, 5}
-- table.map(a, function(v, k, x) return v * k + x end, 100) --produces { 101, 104, 109, 116, 125}
-- @tparam table tbl the table to be mapped to the transform
-- @tparam function func the function to transform values
-- @param[opt] ... additional arguments passed to the function
-- @treturn table a new table containing the keys and mapped values
function Table.map(tbl, func, ...)
local new_tbl = {}
for k, v in pairs(tbl) do new_tbl[k] = func(v, k, ...) end
return new_tbl
end
--- Given a filter function, creates a filtered copy of the table
-- by calling the function for each element in the table, and
-- filtering out any key-value pairs for non-true results. Passes the index as second argument to the function.
-- @usage a= { 1, 2, 3, 4, 5}
-- table.filter(a, function(v) return v % 2 == 0 end) --produces: { 2, 4 }
-- @usage a = {1, 2, 3, 4, 5}
-- table.filter(a, function(v, k, x) return k % 2 == 1 end) --produces: { 1, 3, 5 }
-- @tparam table tbl the table to be filtered
-- @tparam function func the function to filter values
-- @param[opt] ... additional arguments passed to the function
-- @treturn table a new table containing the filtered key-value pairs
function Table.filter(tbl, func, ...)
local new_tbl = {}
local add = #tbl > 0
for k, v in pairs(tbl) do
if func(v, k, ...) then
if add then
Table.insert(new_tbl, v)
else
new_tbl[k] = v
end
end
end
return new_tbl
end
--- Given a candidate search function, iterates over the table, calling the function
-- for each element in the table, and returns the first element the search function returned true.
-- Passes the index as second argument to the function.
-- @usage a= { 1, 2, 3, 4, 5}
-- table.find(a, function(v) return v % 2 == 0 end) --produces: 2
-- @usage a = {1, 2, 3, 4, 5}
-- table.find(a, function(v, k, x) return k % 2 == 1 end) --produces: 1
-- @tparam table tbl the table to be searched
-- @tparam function func the function to use to search for any matching element
-- @param[opt] ... additional arguments passed to the function
-- @treturn ?|nil|Mixed the first found value, or nil if none was found
function Table.find(tbl, func, ...)
for k, v in pairs(tbl) do if func(v, k, ...) then return v, k end end
return nil
end
--- Given a candidate search function, iterates over the table, calling the function
-- for each element in the table, and returns true if search function returned true.
-- Passes the index as second argument to the function.
-- @see table.find
-- @usage a= { 1, 2, 3, 4, 5}
-- table.any(a, function(v) return v % 2 == 0 end) --produces: true
-- @usage a = {1, 2, 3, 4, 5}
-- table.any(a, function(v, k, x) return k % 2 == 1 end) --produces: true
-- @tparam table tbl the table to be searched
-- @tparam function func the function to use to search for any matching element
-- @param[opt] ... additional arguments passed to the function
-- @treturn boolean true if an element was found, false if none was found
function Table.any(tbl, func, ...)
return Table.find(tbl, func, ...) ~= nil
end
--- Given a candidate search function, iterates over the table, calling the function
-- for each element in the table, and returns true if search function returned true
-- for all items in the table.
-- Passes the index as second argument to the function.
-- @tparam table tbl the table to be searched
-- @tparam function func the function to used to search
-- @param[opt] ... additional arguments passed to the search function
-- @treturn boolean true if all elements in the table return truthy
function Table.all(tbl, func, ...)
for k, v in pairs(tbl) do if not func(v, k, ...) then return false end end
return true
end
--- Given a function, apply it to each element in the table.
-- Passes the index as the second argument to the function.
-- <p>Iteration is aborted if the applied function returns true for any element during iteration.
-- @usage
-- a = {10, 20, 30, 40}
-- table.each(a, function(v) game.print(v) end) --prints 10, 20, 30, 40, 50
-- @tparam table tbl the table to be iterated
-- @tparam function func the function to apply to elements
-- @param[opt] ... additional arguments passed to the function
-- @treturn table the table where the given function has been applied to its elements
function Table.each(tbl, func, ...)
for k, v in pairs(tbl) do if func(v, k, ...) then break end end
return tbl
end
--- Returns a new array that is a one-dimensional recursive flattening of the given array.
-- For every element that is an array, extract its elements into the new array.
-- <p>The optional level argument determines the level of recursion to flatten.
-- > This function flattens an integer-indexed array, but not an associative array.
-- @tparam array tbl the array to be flattened
-- @tparam[opt] uint level recursive levels, or no limit to recursion if not supplied
-- @treturn array a new array that represents the flattened contents of the given array
function Table.flatten(tbl, level)
local flattened = {}
Table.each(tbl, function(value)
if type(value) == 'table' and #value > 0 then
if level then
if level > 0 then
Table.merge(flattened, Table.flatten(value, level - 1), true)
else
Table.insert(flattened, value)
end
else
Table.merge(flattened, Table.flatten(value), true)
end
else
Table.insert(flattened, value)
end
end)
return flattened
end
--- Given an array, returns the first element or nil if no element exists.
-- @tparam array tbl the array
-- @treturn ?|nil|Mixed the first element
function Table.first(tbl)
return tbl[1]
end
--- Given an array, returns the last element or nil if no elements exist.
-- @tparam array tbl the array
-- @treturn ?|nil|Mixed the last element or nil
function Table.last(tbl)
local size = #tbl
if size == 0 then return nil end
return tbl[size]
end
--- Given an array of only numeric values, returns the minimum or nil if no element exists.
-- @tparam {number,...} tbl the array with only numeric values
-- @treturn ?|nil|number the minimum value
function Table.min(tbl)
if #tbl == 0 then return nil end
local min = tbl[1]
for _, num in pairs(tbl) do min = num < min and num or min end
return min
end
---Given an array of only numeric values, returns the maximum or nil if no element exists.
-- @tparam {number,...} tbl the array with only numeric values
-- @treturn ?|nil|number the maximum value
function Table.max(tbl)
if #tbl == 0 then return nil end
local max = tbl[1]
for _, num in pairs(tbl) do max = num > max and num or max end
return max
end
--- Given an array of only numeric values, return the sum of all values, or 0 for empty arrays.
-- @tparam {number,...} tbl the array with only numeric values
-- @treturn number the sum of the numbers or zero if the given array was empty
function Table.sum(tbl)
local sum = 0
for _, num in pairs(tbl) do sum = sum + num end
return sum
end
--- Given an array of only numeric values, returns the average or nil if no element exists.
-- @tparam {number,...} tbl the array with only numeric values
-- @treturn ?|nil|number the average value
function Table.avg(tbl)
local cnt = #tbl
return cnt ~= 0 and Table.sum(tbl) / cnt or nil
end
--- Return a new array slice.
-- @tparam array tbl the table to slice
-- @tparam[opt=1] number start
-- @tparam[opt=#tbl] number stop stop at this index, use negative to stop from end.
-- @usage local tab = { 10, 20, 30, 40, 50}
-- slice(tab, 2, -2) --returns { 20, 30, 40 }
function Table.slice(tbl, start, stop)
local res = {}
local n = #tbl
start = start or 1
stop = stop or n
stop = stop < 0 and (n + stop + 1) or stop
if start < 1 or start > n then return {} end
local k = 1
for i = start, stop do
res[k] = tbl[i]
k = k + 1
end
return res
end
--- Merges two tables, values from first get overwritten by the second.
-- @usage
-- function some_func(x, y, args)
-- args = table.merge({option1=false}, args)
-- if opts.option1 == true then return x else return y end
-- end
-- some_func(1,2) -- returns 2
-- some_func(1,2,{option1=true}) -- returns 1
-- @tparam table tblA first table
-- @tparam table tblB second table
-- @tparam[opt=false] boolean array_merge set to true to merge the tables as an array or false for an associative array
-- @tparam[opt=false] boolean raw use rawset for associated array
-- @treturn array|table an array or an associated array where tblA and tblB have been merged
function Table.merge(tblA, tblB, array_merge, raw)
if not tblB then return tblA end
if array_merge then
for _, v in pairs(tblB) do Table.insert(tblA, v) end
else
for k, v in pairs(tblB) do
if raw then
rawset(tblA, k, v)
else
tblA[k] = v
end
end
end
return tblA
end
function Table.array_combine(...)
local tables = { ... }
local new = {}
for _, tab in pairs(tables) do for _, v in pairs(tab) do Table.insert(new, v) end end
return new
end
function Table.dictionary_combine(...)
local tables = { ... }
local new = {}
for _, tab in pairs(tables) do for k, v in pairs(tab) do new[k] = v end end
return new
end
--- Creates a new merged dictionary, if the values in tbl_b are in tbl_a they are not overwritten.
-- @usage
-- local a = {one = A}
-- local b = {one = Z, two = B}
-- local merged = table.dictionary_merge(tbl_a, tbl_b)
-- --merged = {one = A, two = B}
-- @tparam table tbl_a
-- @tparam table tbl_b
-- @treturn table with a and b merged together
function Table.dictionary_merge(tbl_a, tbl_b)
local meta_a = getmetatable(tbl_a)
local meta_b = getmetatable(tbl_b)
setmetatable(tbl_a, nil)
setmetatable(tbl_b, nil)
local new_t = {}
for k, v in pairs(tbl_a) do new_t[k] = v end
for k, v in pairs(tbl_b or {}) do if not new_t[k] then new_t[k] = v end end
setmetatable(tbl_a, meta_a)
setmetatable(tbl_b, meta_b)
return new_t
end
--- Compares 2 tables for inner equality.
-- Modified from factorio/data/core/lualib/util.lua
-- @tparam table t1
-- @tparam table t2
-- @tparam[opt=false] boolean ignore_mt ignore eq metamethod
-- @treturn boolean if the tables are the same
-- @author Sparr, Nexela, luacode.org
function Table.deep_compare(t1, t2, ignore_mt)
local ty1, ty2 = type(t1), type(t2)
if ty1 ~= ty2 then return false end
-- non-table types can be directly compared
if ty1 ~= 'table' and ty2 ~= 'table' then return t1 == t2 end
-- as well as tables which have the metamethod __eq
if not ignore_mt then
local mt = getmetatable(t1)
if mt and mt.__eq then return t1 == t2 end
end
for k1, v1 in pairs(t1) do
local v2 = t2[k1]
if v2 == nil or not Table.deep_compare(v1, v2) then return false end
end
for k in pairs(t2) do if t1[k] == nil then return false end end
return true
end
Table.compare = Table.deep_compare
--- Creates a deep copy of table without copying Factorio objects.
-- copied from factorio/data/core/lualib/util.lua
-- @usage local copy = table.deep_copy[data.raw.["stone-furnace"]["stone-furnace"]]
-- -- returns a copy of the stone furnace entity
-- @tparam table object the table to copy
-- @treturn table a copy of the table
function Table.deep_copy(object)
local lookup_table = {}
local function _copy(inner)
if type(inner) ~= 'table' then
return inner
elseif inner.__self then
return inner
elseif lookup_table[inner] then
return lookup_table[inner]
end
local new_table = {}
lookup_table[inner] = new_table
for index, value in pairs(inner) do new_table[_copy(index)] = _copy(value) end
return setmetatable(new_table, getmetatable(inner))
end
return _copy(object)
end
Table.deepcopy = Table.deep_copy
--- Creates a deep copy of a table without copying factorio objects
-- internal table refs are also deepcopy. The resulting table should
-- @usage local copy = table.fullcopy[data.raw.["stone-furnace"]["stone-furnace"]]
-- -- returns a deepcopy of the stone furnace entity with no internal table references.
-- @tparam table object the table to copy
-- @treturn table a copy of the table
function Table.full_copy(object)
local lookup_table = {}
local function _copy(inner)
if type(inner) ~= 'table' then
return inner
elseif inner.__self then
return inner
elseif lookup_table[inner] then
return _copy(lookup_table[inner])
end
local new_table = {}
lookup_table[inner] = new_table
for index, value in pairs(inner) do new_table[_copy(index)] = _copy(value) end
return setmetatable(new_table, getmetatable(inner))
end
return _copy(object)
end
Table.fullcopy = Table.full_copy
--- Creates a flexible deep copy of an object, recursively copying sub-objects
-- @usage local copy = table.flexcopy(data.raw.["stone-furnace"]["stone-furnace"])
-- -- returns a copy of the stone furnace entity
-- @tparam table object the table to copy
-- @treturn table a copy of the table
function Table.flex_copy(object)
local lookup_table = {}
local function _copy(inner)
if type(inner) ~= 'table' then
return inner
elseif inner.__self then
return inner
elseif lookup_table[inner] then
return lookup_table[inner]
elseif type(inner._copy_with) == 'function' then
lookup_table[inner] = inner:_copy_with(_copy)
return lookup_table[inner]
end
local new_table = {}
lookup_table[inner] = new_table
for index, value in pairs(inner) do new_table[_copy(index)] = _copy(value) end
return setmetatable(new_table, getmetatable(inner))
end
return _copy(object)
end
Table.flexcopy = Table.flex_copy
--- Returns a copy of all of the values in the table.
-- @tparam table tbl the table to copy the keys from, or an empty table if tbl is nil
-- @tparam[opt] boolean sorted whether to sort the keys (slower) or keep the random order from pairs()
-- @tparam[opt] boolean as_string whether to try and parse the values as strings, or leave them as their existing type
-- @treturn array an array with a copy of all the values in the table
function Table.values(tbl, sorted, as_string)
if not tbl then return {} end
local value_set = {}
local n = 0
if as_string then -- checking as_string /before/ looping is faster
for _, v in pairs(tbl) do
n = n + 1
value_set[n] = tostring(v)
end
else
for _, v in pairs(tbl) do
n = n + 1
value_set[n] = v
end
end
if sorted then
table.sort(value_set, function(x, y) -- sorts tables with mixed index types.
local tx = type(x) == 'number'
local ty = type(y) == 'number'
if tx == ty then
return x < y and true or false -- similar type can be compared
elseif tx == true then
return true -- only x is a number and goes first
else
return false -- only y is a number and goes first
end
end)
end
return value_set
end
--- Returns a copy of all of the keys in the table.
-- @tparam table tbl the table to copy the keys from, or an empty table if tbl is nil
-- @tparam[opt] boolean sorted whether to sort the keys (slower) or keep the random order from pairs()
-- @tparam[opt] boolean as_string whether to try and parse the keys as strings, or leave them as their existing type
-- @treturn array an array with a copy of all the keys in the table
function Table.keys(tbl, sorted, as_string)
if not tbl then return {} end
local key_set = {}
local n = 0
if as_string then -- checking as_string /before/ looping is faster
for k, _ in pairs(tbl) do
n = n + 1
key_set[n] = tostring(k)
end
else
for k, _ in pairs(tbl) do
n = n + 1
key_set[n] = k
end
end
if sorted then
table.sort(key_set, function(x, y) -- sorts tables with mixed index types.
local tx = type(x) == 'number'
local ty = type(y) == 'number'
if tx == ty then
return x < y and true or false -- similar type can be compared
elseif tx == true then
return true -- only x is a number and goes first
else
return false -- only y is a number and goes first
end
end)
end
return key_set
end
--- Removes keys from a table by setting the values associated with the keys to nil.
-- @usage local a = {1, 2, 3, 4}
-- table.remove_keys(a, {1,3}) --returns {nil, 2, nil, 4}
-- @usage local b = {k1 = 1, k2 = 'foo', old_key = 'bar'}
-- table.remove_keys(b, {'old_key'}) --returns {k1 = 1, k2 = 'foo'}
-- @tparam table tbl the table to remove the keys from
-- @tparam {Mixed,...} keys an array of keys that exist in the given table
-- @treturn table tbl without the specified keys
function Table.remove_keys(tbl, keys)
for i = 1, #keys do tbl[keys[i]] = nil end
return tbl
end
--- Returns the number of keys in a table, if func is passed only count keys when the function is true.
-- @tparam table tbl to count keys
-- @tparam[opt] function func to increment counter
-- @param[optchain] ... additional arguments passed to the function
-- @treturn number The number of keys matching the function or the number of all keys if func isn't passed
-- @treturn number The total number of keys
-- @usage local a = { 1, 2, 3, 4, 5}
-- table.count_keys(a) -- produces: 5, 5
-- @usage local a = {1, 2, 3, 4, 5}
-- table.count_keys(a, function(v, k) return k % 2 == 1 end) -- produces: 3, 5
function Table.count_keys(tbl, func, ...)
local count, total = 0, 0
if type(tbl) == 'table' then
for k, v in pairs(tbl) do
total = total + 1
if func then
if func(v, k, ...) then count = count + 1 end
else
count = count + 1
end
end
end
return count, total
end
--- Returns an inverted (***{[value] = key,...}***) copy of the given table. If the values are not unique,
-- the assigned key depends on the order of pairs().
-- @usage local a = {k1 = 'foo', k2 = 'bar'}
-- table.invert(a) --returns {'foo' = k1, 'bar' = k2}
-- @usage local b = {k1 = 'foo', k2 = 'bar', k3 = 'bar'}
-- table.invert(b) --returns {'foo' = k1, 'bar' = ?}
-- @tparam table tbl the table to invert
-- @treturn table a new table with inverted mapping
function Table.invert(tbl)
local inverted = {}
for k, v in pairs(tbl) do inverted[v] = k end
return inverted
end
local function _size(tbl)
local count = 0
for _ in pairs(tbl or {}) do count = count + 1 end
return count
end
--- Return the size of a table using the factorio built in table_size function
-- @function size
-- @tparam table table to use
-- @treturn int size of the table
Table.size = _ENV.table_size or _size
--- For all string or number values in an array map them to a value = value table
-- @usage local a = {"v1", "v2"}
-- table.array_to_bool(a) -- return {["v1"] = "v1", ["v2"]= "v2"}
-- @tparam table tbl the table to convert
-- @tparam[opt=false] boolean as_bool map to true instead of value
-- @treturn table the converted table
function Table.array_to_dictionary(tbl, as_bool)
local new_tbl = {}
for _, v in ipairs(tbl) do
if type(v) == 'string' or type(v) == 'number' then new_tbl[v] = as_bool and true or v end
end
return new_tbl
end
-- Returns an array of unique values from tbl
-- @tparam table tbl
-- @treturn table an array of unique values.
function Table.unique_values(tbl)
return Table.keys(Table.invert(tbl))
end
--- Does the table contain any elements
-- @tparam table tbl
-- @treturn boolean
function Table.is_empty(tbl)
return _ENV.table_size and _ENV.table_size(tbl) == 0 or next(tbl) == nil
end
--- Clear all elements in a table
-- @tparam table tbl the table to clear
-- @treturn table the cleared table
function Table.clear(tbl)
for k in pairs(tbl) do tbl[k] = nil end
return tbl
end
return Table

View File

@@ -0,0 +1,11 @@
local type = type
local Type = {
Table = function(param) return type(param) == 'table' end,
Function = function(param) return type(param) == 'function' or type((getmetatable(param) or {}).__call) == 'function' end,
Nil = function(param) return param == nil end,
String = function(param) return type(param) == 'string' end,
Number = function(param) return type(param) == 'number' end,
}
return Type

View File

@@ -0,0 +1,576 @@
--- A collection library to simplify sequential table operations
--
-- local Enumerable = require('__stdlib__/stdlib/vendor/enumerable')
-- Enumerable.create({1,2,3})
-- @classmod Vendor.Enumerable
-- @author Billiam
local Enumerable = {}
Enumerable.mt = {
__index = function(self, key)
return Enumerable[key] or self._data[key]
end
}
--- Tests tables for numeric keys with no gaps
-- @tparam table t Table to check
-- @treturn boolean True if table is sequence-like, false otherwise
local function isSequence(t)
local i = 0
for _ in pairs(t) do
i = i + 1
if t[i] == nil then return false end
end
return true
end
--- Whether the item passed in may be called as a function
-- @param f Item to test for callability
-- @treturn boolean True if passed in item may be called, false otherwise
local function isCallable(f)
local t = type(f)
if t == 'function' then
return true
end
if t == 'table' then
local meta = getmetatable(f)
return meta and type(meta.__call) == 'function'
end
return false
end
--- Enumerable constructor.
-- If no collection is provided, a new empty table will be generated.
--
-- @usage
-- collection = Enumerable.create({123})
-- @tparam ?table collection Sequential table to wrap
-- @treturn enumerable A new collection instance
-- @raise 'Enumerable data must be a sequence' if given a non-sequential table
function Enumerable.create(collection)
if collection and not (type(collection) == 'table' and isSequence(collection)) then
error('Enumerable data must be a sequence')
end
local instance = {
--- Internal collection data
-- @table _data
_data = collection or {}
}
setmetatable(instance, Enumerable.mt)
return instance
end
--- Return the unwrapped collection data.
-- @usage
-- collectionData = collection:to_table()
-- @treturn table
function Enumerable:data()
return self._data
end
--- Create a shallow copy of the unwrapped collection data.
-- @usage
-- clonedData = collection:to_table()
-- @treturn table
function Enumerable:to_table()
local meta = getmetatable(self._data)
local target = {}
for k, v in pairs(self._data) do target[k] = v end
setmetatable(target, meta)
return target
end
--- Pass all elements in the collection to a callback.
-- @usage
-- collection:each(function(value, index) ... end)
-- @tparam callable callback
-- @treturn enumerable The collection instance
function Enumerable:each(callback)
for i,v in ipairs(self._data) do
callback(v, i)
end
return self
end
--- Pass all elements in collection to a callback.
-- returns a new enumerable instance containing values
-- returned by the callback.
-- @usage
-- collection = Enumerable.create({1, 2, 3})
-- collection:map(function(value, index) return value* 2 end)
-- -> Enumerable containing {2, 4, 6}
-- @tparam callable callback
-- @treturn enumerable New enumerable instance
function Enumerable:map(callback)
local map = {}
for i,v in ipairs(self._data) do
local result = callback(v, i)
if result ~= nil then
table.insert(map, result)
end
end
return Enumerable.create(map)
end
--- Find the position of the first item in collection for which
-- the callback returns true.
-- @usage
-- collection = Collection.create({0, 1, 2, 3, 4})
-- collection:findIndex(function(value, index) return value > 2 end)
-- -> 4
-- @tparam callable callback
-- @treturn int the position of the matched element
function Enumerable:find_index(callback)
for i,v in ipairs(self._data) do
if callback(v, i) then
return i
end
end
end
--- Whether the collection has no elements.
-- @usage collection = Enumerable.create()
-- if collection:empty() then
-- print('Collection is empty')
-- end
-- -> Collection is empty
-- @treturn bool
function Enumerable:empty()
return #self._data == 0
end
--- Return the first element or elements in the collection.
-- @see Enumerable:last
-- @usage
-- collection = Enumerable.create({1,2,3,4})
-- collection:first()
-- -> 1
-- collection:first(3)
-- -> {1,2,3}
-- @tparam ?int n Number of elements to return. If absent, the first item will be returned.
-- @treturn table|*
function Enumerable:first(n)
if not n or n == 1 then
return self._data[1]
end
local list = {}
n = math.min(n, #self._data)
for i=1,n do
table.insert(list, self._data[i])
end
return list
end
--- Return the last element or elements in the collection.
-- @see Enumerable:first
-- @usage
-- collection = Enumerable.create({1,2,3,4})
-- collection:last()
-- -> 4
-- collection:last(3)
-- -> {2, 3, 4}
-- @tparam ?int n Number of elements to return. If absent, the last item will be returned.
-- @treturn table
function Enumerable:last(n)
if not n or n == 1 then
return self._data[#self._data]
end
local list = {}
n = math.max(1, #self._data - (n - 1))
for i=n,#self._data do
table.insert(list, self._data[i])
end
return list
end
--- Return the number of items in the collection.
-- If a callback is given, count will return the
-- number of elements for which the callback returns true
-- @usage
-- collection = Enumerable.create({1,2,3})
-- collection:count()
-- -> 3
-- collection:count(function(value, index) return value % 2 == 0 end)
-- -> 1
-- @tparam callable callback Callback used to determine whether element should be counted
-- @treturn int
function Enumerable:count(callback)
if not callback then
return #self._data
end
local count = 0
for i,v in ipairs(self._data) do
if callback(v, i) then
count = count + 1
end
end
return count
end
--- Append the contents of one table onto the end of the existing enumerable.
-- @usage
-- pets = Enumerable:create({'dog', 'cat'})
-- pets:concat({'turtle', 'wizard'})
-- -> pets now contains {'dog', 'cat', 'turtle', 'wizard'}
-- @tparam table other Table with content to append to enumerable
-- @treturn enumerable The enumerable instance
function Enumerable:concat(other)
return self:push(unpack(other))
end
--- Combine elements of enumerable by passing all items to a callback.
-- Values returned by the callback will be used as the accumulator value
-- for subsequent callbacks.
-- @usage
-- numbers = Enumerable.create({1,2,3})
-- numbers:reduce(function(accumulator, value) return (accumulator or 0) + value end)
-- -> 6
-- numbers:reduce(20, function(accumulator, value) return accumulator + value end)
-- -> 26
-- @tparam ?int initial Initial value for accumulator
-- @tparam callable callback
-- @return Accumulator value
function Enumerable:reduce(initial, callback)
if not callback then
if isCallable(initial) then
callback = initial
initial = nil
else
error('Callback must be a function or table with a __call metamethod')
end
end
local reduce = initial
for i,v in ipairs(self._data) do
reduce = callback(reduce, v, i)
end
return reduce
end
--- Find the lowest value in the enumerable instance.
-- If a callback is provided, the return value will be used
-- to determine the lowest value.
-- @usage
-- strings = Enumerable.create({'aaaaaa', 'bbb', 'c'})
-- strings:min()
-- -> 'aaaaa'
-- strings:min(function(value) return #value end)
-- -> 'c'
-- @tparam ?callable callback
-- @return Lowest value
function Enumerable:min(callback)
callback = callback or function(v) return v end
local lowestValue
return self:reduce(function(output, v)
local result = callback(v)
if not output or (result and result < lowestValue) then
lowestValue = result
return v
end
return output
end)
end
--- Find the highest value in the enumerable instance.
-- If a callback is provided, the return value will be used
-- to determine the highest value.
-- @usage
-- strings = Enumerable.create({'aaaaaa', 'bbb', 'c'})
-- strings:max()
-- -> 'c'
-- strings:max(function(value) return #value end)
-- -> 'aaaaa'
-- @tparam ?callable callback
-- @return Highest value
function Enumerable:max(callback)
callback = callback or function(v) return v end
local highestValue
return self:reduce(function(output, v)
local result = callback(v)
if not output or (result and result > highestValue) then
highestValue = result
return v
end
return output
end)
end
--- Find the highest and lowest values in the enumerable.
-- If a callback is provided, the return value will be used
-- to determine the highest and lowest values.
-- @usage
-- numbers = Enumerable.create({6,3,1,5,2,4})
-- lowest, highest = numbers:minmax()
-- -> (1,6)
-- strings:max(function(value) return 10 - value end)
-- -> (6, 1)
-- @tparam ?callable callback
-- @return Lowest value
-- @return Highest value
function Enumerable:minmax(callback)
return self:min(callback), self:max(callback)
end
--- Sort the enumerable by optional callback in place.
-- If a callback is not provided, data will be sorted in ascending order.
-- If callback is provided, it will be passed two table elements, and should
-- return true if the first element should appear first, otherwise false.
-- See also: [table.sort documentation](http://www.lua.org/manual/5.1/manual.html#pdf-table.sort)
-- @usage
-- numbers = Enumerable.create({2,1,3})
-- numbers:sort()
-- -> numbers now contains {1,2,3}
-- numbers:sort(function(a, b) return b < a end)
-- -> numbers now contains {3,2,1}
-- @tparam ?callable callback sort method
-- @treturn enumerable The collection instance
function Enumerable:sort(callback)
table.sort(self._data, callback)
return self
end
--- Add one or more items to the enumerable.
-- @usage
-- items = Enumerable.create({1,2,3})
-- items:push(4, 5)
-- -> items contains {1,2,3,4,5}
-- @param ... Items to append
-- @treturn enumerable The collection instance
function Enumerable:push(...)
for i,v in ipairs({...}) do
table.insert(self._data, v)
end
return self
end
--- Remove and return the last item from the collection.
-- @usage
-- items = Enumerable.create({1,2,3})
-- items:pop()
-- -> returns 3
-- -> items now contains {1,2}
-- @treturn enumerable The collection instance
function Enumerable:pop()
return table.remove(self._data, #self._data)
end
--- Remove and return the first item from the collection.
-- @usage
-- items = Enumerable.create({1,2,3})
-- items:shift()
-- -> returns 1
-- -> items now contains {2,3}
-- @treturn enumerable The collection instance
function Enumerable:shift()
return table.remove(self._data, 1)
end
--- Insert one or more items into the beginning of the collection.
-- @usage
-- items = Enumerable.create({4,5,6})
-- items:unshift(1,2,3)
-- -> Items now contains {1,2,3,4,5,6}
-- @param ... Elements to insert
-- @treturn enumerable The collection instance
function Enumerable:unshift(...)
for i,v in ipairs({...}) do
table.insert(self._data, i, v)
end
return self
end
--- Returns the first element in the collection where the callback returns true.
-- @usage
-- numbers = Enumerable.create({20, 30, 40})
-- numbers:find(function(value, index) return value > 25 end)
-- -> 30
-- @tparam callable callback
-- @return Matching item
function Enumerable:find(callback)
for i,v in ipairs(self._data) do
if callback(v, i) then
return v
end
end
end
--- Create a new collection with elements which the callback returns false.
-- @usage
-- items = Enumerable.create({1,2,3,4,5,6})
-- odd = Enumerable:reject(function(value, index) return value % 2 == 0 end)
-- -> Enumerable containing {1,3,5}
-- @tparam callable callback
-- @treturn enumerable New collection instance
function Enumerable:reject(callback)
local reject = {}
for i,v in ipairs(self._data) do
if not callback(v, i) then
table.insert(reject, v)
end
end
return Enumerable.create(reject)
end
--- Create a new collection with elements which the callback returns true.
-- @usage
-- items = Enumerable.create({1,2,3,4,5,6})
-- even = Enumerable:select(function(value, index) return value % 2 == 0 end)
-- -> Enumerable containing {2,4,6}
-- @tparam callable callback
-- @treturn enumerable New collection instance
-- @alias find_all
function Enumerable:select(callback)
local select = {}
for i,v in ipairs(self._data) do
if callback(v, i) then
table.insert(select, v)
end
end
return Enumerable.create(select)
end
--- Returns true if callback returns truthy for all elements in the collection.
-- @usage
-- items = Enumerable.create({10, 20, 30})
-- items:all(function(value, index) return value > 5 end)
-- -> true
-- items:all(function(value, index) return value < 25 end)
-- -> false
-- @tparam callable callback
-- @treturn bool
function Enumerable:all(callback)
for i,v in ipairs(self._data) do
if not callback(v, i) then
return false
end
end
return true
end
--- Returns true if callback returns truthy for any element in the collection.
-- @usage
-- items = Enumerable.create({10, 20, 30})
-- items:any(function(value, index) return value > 25 end)
-- -> true
-- items:any(function(value, index) return value > 30 end)
-- -> false
-- @tparam callable callback
-- @treturn bool
function Enumerable:any(callback)
for i,v in ipairs(self._data) do
if callback(v, i) then
return true
end
end
return false
end
--- Groups elements into collections based on the result of the provided callback.
-- Resulting table will have keys matching the returned value of the callback, and
-- values as a table of elements which returned that value.
-- @usage
-- numbers = Enumerable.create({1,2,3,4,5,6})
-- result = Enumerable.group_by(function(value, index) return value % 3 end)
-- result[0]
-- -> Enumerable containing {3, 6}
-- result[1]
-- -> Enumerable containing {2, 5}
-- result[2]
-- -> Enumerable containing {1, 4}
-- @tparam callable callback
-- @treturn table
function Enumerable:group_by(callback)
local groups = {}
for i,v in ipairs(self._data) do
local criteria = callback(v, i)
groups[criteria] = groups[criteria] or Enumerable.create()
groups[criteria]:push(v)
end
return groups
end
--- Split enumerable into two groups, based on the true or false result of the callback.
--
-- Aliases: find_all, detect
--
-- @usage
-- numbers = Enumerable.create({1,2,3,4,5,6})
-- even, odd = Enumerable:partition(function(value, index) return value % 2 == 1 end)
-- -> even is a collection containing {2, 4, 6}
-- -> odd is a collection containing {1, 3, 5}
-- @tparam callable callback
-- @treturn enumerable Collection containing items which returned true
-- @treturn enumerable Collection containing items which returned false
function Enumerable:partition(callback)
local truthyCallback = function(v, i)
return callback(v, i) and true or false
end
local results = self:group_by(truthyCallback)
return results[true] or Enumerable.create(), results[false] or Enumerable.create()
end
---
-- @function Enumerable:find_all
-- @see Enumerable:select
Enumerable.find_all = Enumerable.select
---
-- @function Enumerable:detect
-- @see Enumerable:select
Enumerable.detect = Enumerable.select
---
-- @function Enumerable:collect
-- @see Enumerable:map
Enumerable.collect = Enumerable.map
---
-- @function Enumerable:inject
-- @see Enumerable:reduce
Enumerable.inject = Enumerable.reduce
return Enumerable

418
stdlib_1.4.8/stdlib/vendor/inspect.lua vendored Normal file
View File

@@ -0,0 +1,418 @@
local inspect = {
_VERSION = 'inspect.lua 3.1.0',
_URL = 'http://github.com/kikito/inspect.lua',
_DESCRIPTION = 'human-readable representations of tables',
_LICENSE = [[
MIT LICENSE
Copyright (c) 2013 Enrique García Cota
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
]]
}
local tostring = tostring
inspect.KEY =
setmetatable(
{},
{
__tostring = function()
return 'inspect.KEY'
end
}
)
inspect.METATABLE =
setmetatable(
{},
{
__tostring = function()
return 'inspect.METATABLE'
end
}
)
-- Apostrophizes the string if it has quotes, but not aphostrophes
-- Otherwise, it returns a regular quoted string
local function smartQuote(str)
if str:match('"') and not str:match("'") then
return "'" .. str .. "'"
end
return '"' .. str:gsub('"', '\\"') .. '"'
end
-- \a => '\\a', \0 => '\\0', 31 => '\31'
local shortControlCharEscapes = {
['\a'] = '\\a',
['\b'] = '\\b',
['\f'] = '\\f',
['\n'] = '\\n',
['\r'] = '\\r',
['\t'] = '\\t',
['\v'] = '\\v'
}
local longControlCharEscapes = {} -- \a => nil, \0 => \000, 31 => \031
for i = 0, 31 do
local ch = string.char(i)
if not shortControlCharEscapes[ch] then
shortControlCharEscapes[ch] = '\\' .. i
longControlCharEscapes[ch] = string.format('\\%03d', i)
end
end
local function escape(str)
return (str:gsub('\\', '\\\\'):gsub('(%c)%f[0-9]', longControlCharEscapes):gsub('%c', shortControlCharEscapes))
end
local function isIdentifier(str)
return type(str) == 'string' and str:match('^[_%a][_%a%d]*$')
end
local function isSequenceKey(k, sequenceLength)
return type(k) == 'number' and 1 <= k and k <= sequenceLength and math.floor(k) == k
end
local defaultTypeOrders = {
['number'] = 1,
['boolean'] = 2,
['string'] = 3,
['table'] = 4,
['function'] = 5,
['userdata'] = 6,
['thread'] = 7
}
local function sortKeys(a, b)
local ta, tb = type(a), type(b)
-- strings and numbers are sorted numerically/alphabetically
if ta == tb and (ta == 'string' or ta == 'number') then
return a < b
end
local dta, dtb = defaultTypeOrders[ta], defaultTypeOrders[tb]
-- Two default types are compared according to the defaultTypeOrders table
if dta and dtb then
return defaultTypeOrders[ta] < defaultTypeOrders[tb]
elseif dta then
return true -- default types before custom ones
elseif dtb then
return false -- custom types after default ones
end
-- custom types are sorted out alphabetically
return ta < tb
end
-- For implementation reasons, the behavior of rawlen & # is "undefined" when
-- tables aren't pure sequences. So we implement our own # operator.
local function getSequenceLength(t)
local len = 1
local v = rawget(t, len)
while v ~= nil do
len = len + 1
v = rawget(t, len)
end
return len - 1
end
local function getNonSequentialKeys(t)
local keys = {}
local sequenceLength = getSequenceLength(t)
for k, _ in pairs(t) do
if not isSequenceKey(k, sequenceLength) then
table.insert(keys, k)
end
end
table.sort(keys, sortKeys)
return keys, sequenceLength
end
local function getToStringResultSafely(t, mt)
local __tostring = type(mt) == 'table' and rawget(mt, '__tostring')
local str, ok
if type(__tostring) == 'function' then
ok, str = pcall(__tostring, t)
str = ok and str or 'error: ' .. tostring(str)
end
if type(str) == 'string' and #str > 0 then
return str
end
end
local function countTableAppearances(t, tableAppearances)
tableAppearances = tableAppearances or {}
if type(t) == 'table' then
if not tableAppearances[t] then
tableAppearances[t] = 1
for k, v in pairs(t) do
countTableAppearances(k, tableAppearances)
countTableAppearances(v, tableAppearances)
end
countTableAppearances(getmetatable(t), tableAppearances)
else
tableAppearances[t] = tableAppearances[t] + 1
end
end
return tableAppearances
end
local copySequence = function(s)
local copy, len = {}, #s
for i = 1, len do
copy[i] = s[i]
end
return copy, len
end
local function makePath(path, ...)
local keys = {...}
local newPath, len = copySequence(path)
for i = 1, #keys do
newPath[len + i] = keys[i]
end
return newPath
end
local function processRecursive(process, item, path, visited)
if item == nil then
return nil
end
if visited[item] then
return visited[item]
end
local processed = process(item, path)
if type(processed) == 'table' then
local processedCopy = {}
visited[item] = processedCopy
local processedKey
for k, v in pairs(processed) do
processedKey = processRecursive(process, k, makePath(path, k, inspect.KEY), visited)
if processedKey ~= nil then
processedCopy[processedKey] = processRecursive(process, v, makePath(path, processedKey), visited)
end
end
local mt = processRecursive(process, getmetatable(processed), makePath(path, inspect.METATABLE), visited)
if type(mt) ~= 'table' then
mt = nil
end -- ignore not nil/table __metatable field
setmetatable(processedCopy, mt)
processed = processedCopy
end
return processed
end
-------------------------------------------------------------------
local Inspector = {}
local Inspector_mt = {__index = Inspector}
function Inspector:puts(...)
local args = {...}
local buffer = self.buffer
local len = #buffer
for i = 1, #args do
len = len + 1
buffer[len] = args[i]
end
end
function Inspector:down(f)
self.level = self.level + 1
f()
self.level = self.level - 1
end
function Inspector:tabify()
self:puts(self.newline, string.rep(self.indent, self.level))
end
function Inspector:alreadyVisited(v)
return self.ids[v] ~= nil
end
function Inspector:getId(v)
local id = self.ids[v]
if not id then
local tv = type(v)
id = (self.maxIds[tv] or 0) + 1
self.maxIds[tv] = id
self.ids[v] = id
end
return tostring(id)
end
function Inspector:putKey(k, arraykey)
if arraykey or (not self.longkeys and isIdentifier(k)) then
return self:puts(k)
end
self:puts(arraykey and '' or '[')
self:putValue(k)
self:puts(arraykey and '' or ']')
end
function Inspector:putTable(t)
if t == inspect.KEY or t == inspect.METATABLE then
self:puts(tostring(t))
elseif self:alreadyVisited(t) then
self:puts('<table ', self:getId(t), '>')
elseif self.level >= self.depth then
self:puts('{...}')
else
if self.tableAppearances[t] > 1 then
self:puts('<', self:getId(t), '>')
end
local nonSequentialKeys, sequenceLength = getNonSequentialKeys(t)
local mt = getmetatable(t)
local toStringResult = getToStringResultSafely(t, mt)
self:puts('{')
self:down(
function()
if toStringResult then
self:puts(' -- ', escape(toStringResult))
if sequenceLength >= 1 then
self:tabify()
end
end
local count = 0
for i = 1, sequenceLength do
if count > 0 then
self:puts(',')
end
if self.arraytabify then
self:tabify()
else
self:puts(' ')
end
if self.arraykeys then
self:putKey(i, self.arraykeys)
self:puts(' = ')
end
self:putValue(t[i])
count = count + 1
end
for _, k in ipairs(nonSequentialKeys) do
if count > 0 then
self:puts(',')
end
self:tabify()
self:putKey(k)
self:puts(' = ')
self:putValue(t[k])
count = count + 1
end
if type(mt) == 'table' then
if count > 0 then
self:puts(',')
end
self:tabify()
self:puts('<metatable> = ')
self:putValue(mt)
end
end
)
if #nonSequentialKeys > 0 or type(mt) == 'table' or self.arraytabify then -- result is multi-lined. Justify closing }
self:tabify()
elseif sequenceLength > 0 then -- array tables have one extra space before closing }
self:puts(' ')
end
self:puts('}')
end
end
function Inspector:putValue(v)
local tv = type(v)
if tv == 'string' then
self:puts(smartQuote(escape(v)))
elseif tv == 'number' or tv == 'boolean' or tv == 'nil' or tv == 'cdata' or tv == 'ctype' then
self:puts(tostring(v))
elseif tv == 'table' then
self:putTable(v)
else
self:puts('<', tv, ' ', self:getId(v), '>')
end
end
-------------------------------------------------------------------
function inspect.inspect(root, options)
options = options or {}
local depth = options.depth or math.huge
local newline = options.newline or '\n'
local indent = options.indent or ' '
local process = options.process
local longkeys = options.longkeys
local arraykeys = options.arraykeys
local arraytabify = options.arraytabify
local metatables = options.metatables
if process then
root = processRecursive(process, root, {}, {})
end
local inspector =
setmetatable(
{
depth = depth,
level = 0,
buffer = {},
ids = {},
maxIds = {},
newline = newline,
indent = indent,
longkeys = longkeys,
arraykeys = arraykeys,
arraytabify = arraytabify,
metatables = metatables,
tableAppearances = countTableAppearances(root)
},
Inspector_mt
)
inspector:putValue(root)
return table.concat(inspector.buffer)
end
setmetatable(
inspect,
{
__call = function(_, ...)
return inspect.inspect(...)
end
}
)
return inspect

231
stdlib_1.4.8/stdlib/vendor/semver.lua vendored Normal file
View File

@@ -0,0 +1,231 @@
local semver = {
_VERSION = '1.2.1',
_DESCRIPTION = 'semver for Lua',
_URL = 'https://github.com/kikito/semver.lua',
_LICENSE = [[
MIT LICENSE
Copyright (c) 2015 Enrique García Cota
Permission is hereby granted, free of charge, to any person obtaining a
copy of tother software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and tother permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
]]
}
local function checkPositiveInteger(number, name)
assert(number >= 0, name .. ' must be a valid positive number')
assert(math.floor(number) == number, name .. ' must be an integer')
end
local function present(value)
return value and value ~= ''
end
-- splitByDot("a.bbc.d") == {"a", "bbc", "d"}
local function splitByDot(str)
str = str or ''
local t, count = {}, 0
str:gsub(
'([^%.]+)',
function(c)
count = count + 1
t[count] = c
end
)
return t
end
local function parsePrereleaseAndBuildWithSign(str)
local prereleaseWithSign, buildWithSign = str:match('^(-[^+]+)(+.+)$')
if not (prereleaseWithSign and buildWithSign) then
prereleaseWithSign = str:match('^(-.+)$')
buildWithSign = str:match('^(+.+)$')
end
assert(prereleaseWithSign or buildWithSign, ('The parameter %q must begin with + or - to denote a prerelease or a build'):format(str))
return prereleaseWithSign, buildWithSign
end
local function parsePrerelease(prereleaseWithSign)
if prereleaseWithSign then
local prerelease = prereleaseWithSign:match('^-(%w[%.%w-]*)$')
assert(prerelease, ('The prerelease %q is not a slash followed by alphanumerics, dots and slashes'):format(prereleaseWithSign))
return prerelease
end
end
local function parseBuild(buildWithSign)
if buildWithSign then
local build = buildWithSign:match('^%+(%w[%.%w-]*)$')
assert(build, ('The build %q is not a + sign followed by alphanumerics, dots and slashes'):format(buildWithSign))
return build
end
end
local function parsePrereleaseAndBuild(str)
if not present(str) then
return nil, nil
end
local prereleaseWithSign, buildWithSign = parsePrereleaseAndBuildWithSign(str)
local prerelease = parsePrerelease(prereleaseWithSign)
local build = parseBuild(buildWithSign)
return prerelease, build
end
local function parseVersion(str)
local sMajor, sMinor, sPatch, sPrereleaseAndBuild = str:match('^(%d+)%.?(%d*)%.?(%d*)(.-)$')
assert(type(sMajor) == 'string', ('Could not extract version number(s) from %q'):format(str))
local major, minor, patch = tonumber(sMajor), tonumber(sMinor), tonumber(sPatch)
local prerelease, build = parsePrereleaseAndBuild(sPrereleaseAndBuild)
return major, minor, patch, prerelease, build
end
-- return 0 if a == b, -1 if a < b, and 1 if a > b
local function compare(a, b)
return a == b and 0 or a < b and -1 or 1
end
local function compareIds(myId, otherId)
if myId == otherId then
return 0
elseif not myId then
return -1
elseif not otherId then
return 1
end
local selfNumber, otherNumber = tonumber(myId), tonumber(otherId)
if selfNumber and otherNumber then -- numerical comparison
-- numericals are always smaller than alphanums
return compare(selfNumber, otherNumber)
elseif selfNumber then
return -1
elseif otherNumber then
return 1
else
return compare(myId, otherId) -- alphanumerical comparison
end
end
local function smallerIdList(myIds, otherIds)
local myLength = #myIds
local comparison
for i = 1, myLength do
comparison = compareIds(myIds[i], otherIds[i])
if comparison ~= 0 then
return comparison == -1
end
-- if comparison == 0, continue loop
end
return myLength < #otherIds
end
local function smallerPrerelease(mine, other)
if mine == other or not mine then
return false
elseif not other then
return true
end
return smallerIdList(splitByDot(mine), splitByDot(other))
end
local methods = {}
function methods:nextMajor()
return semver(self.major + 1, 0, 0)
end
function methods:nextMinor()
return semver(self.major, self.minor + 1, 0)
end
function methods:nextPatch()
return semver(self.major, self.minor, self.patch + 1)
end
local mt = {__index = methods}
function mt:__eq(other)
return self.major == other.major and self.minor == other.minor and self.patch == other.patch and self.prerelease == other.prerelease
-- notice that build is ignored for precedence in semver 2.0.0
end
function mt:__lt(other)
if self.major ~= other.major then
return self.major < other.major
end
if self.minor ~= other.minor then
return self.minor < other.minor
end
if self.patch ~= other.patch then
return self.patch < other.patch
end
return smallerPrerelease(self.prerelease, other.prerelease)
-- notice that build is ignored for precedence in semver 2.0.0
end
-- This works like the "pessimisstic operator" in Rubygems.
-- if a and b are versions, a ^ b means "b is backwards-compatible with a"
-- in other words, "it's safe to upgrade from a to b"
function mt:__pow(other)
if self.major == 0 then
return self == other
end
return self.major == other.major and self.minor <= other.minor
end
function mt:__tostring()
local buffer = {('%d.%d.%d'):format(self.major, self.minor, self.patch)}
if self.prerelease then
table.insert(buffer, '-' .. self.prerelease)
end
if self.build then
table.insert(buffer, '+' .. self.build)
end
return table.concat(buffer)
end
local function new(major, minor, patch, prerelease, build)
assert(major, 'At least one parameter is needed')
if type(major) == 'string' then
major, minor, patch, prerelease, build = parseVersion(major)
end
patch = patch or 0
minor = minor or 0
checkPositiveInteger(major, 'major')
checkPositiveInteger(minor, 'minor')
checkPositiveInteger(patch, 'patch')
local result = {major = major, minor = minor, patch = patch, prerelease = prerelease, build = build}
return setmetatable(result, mt)
end
setmetatable(
semver,
{
__call = function(_, ...)
return new(...)
end
}
)
semver._VERSION = semver(semver._VERSION)
return semver

146
stdlib_1.4.8/stdlib/vendor/serpent.lua vendored Normal file
View File

@@ -0,0 +1,146 @@
--Serpent version used in factorio
do
local n, v = "serpent", "0.30" -- (C) 2012-17 Paul Kulchenko; MIT License
local c, d = "Paul Kulchenko", "Lua serializer and pretty printer"
local snum = {[tostring(1/0)]='1/0 --[[math.huge]]',[tostring(-1/0)]='-1/0 --[[-math.huge]]',[tostring(0/0)]='0/0'}
local badtype = {thread = true, userdata = true, cdata = true}
local getmetatable = debug and debug.getmetatable or getmetatable
local pairs = function(t) return next, t end -- avoid using __pairs in Lua 5.2+
local keyword, globals, G = {}, {}, (_G or _ENV)
for _,k in ipairs({'and', 'break', 'do', 'else', 'elseif', 'end', 'false',
'for', 'function', 'goto', 'if', 'in', 'local', 'nil', 'not', 'or', 'repeat',
'return', 'then', 'true', 'until', 'while'}) do keyword[k] = true end
for k,v in pairs(G) do globals[v] = k end -- build func to name mapping
for _,g in ipairs({'coroutine', 'debug', 'io', 'math', 'string', 'table', 'os'}) do
for k,v in pairs(type(G[g]) == 'table' and G[g] or {}) do globals[v] = g..'.'..k end end
local function s(t, opts)
local name, indent, fatal, maxnum = opts.name, opts.indent, opts.fatal, opts.maxnum
local sparse, custom, huge = opts.sparse, opts.custom, not opts.nohuge
local space, maxl = (opts.compact and '' or ' '), (opts.maxlevel or math.huge)
local maxlen, metatostring = tonumber(opts.maxlength), opts.metatostring
local iname, comm = '_'..(name or ''), opts.comment and (tonumber(opts.comment) or math.huge)
local numformat = opts.numformat or "%.17g"
local seen, sref, syms, symn = {}, {'local '..iname..'={}'}, {}, 0
local function gensym(val) return '_'..(tostring(tostring(val)):gsub("[^%w]",""):gsub("(%d%w+)",
-- tostring(val) is needed because __tostring may return a non-string value
function(s) if not syms[s] then symn = symn+1; syms[s] = symn end return tostring(syms[s]) end)) end
local function safestr(s) return type(s) == "number" and tostring(huge and snum[tostring(s)] or numformat:format(s))
or type(s) ~= "string" and tostring(s) -- escape NEWLINE/010 and EOF/026
or ("%q"):format(s):gsub("\\010","n"):gsub("\\026","\\\\026") end
local function comment(s,l) return comm and (l or 0) < comm and ' --[['..select(2, pcall(tostring, s))..']]' or '' end
local function globerr(s,l) return globals[s] and globals[s]..comment(s,l) or not fatal
and safestr(select(2, pcall(tostring, s))) or error("Can't serialize "..tostring(s)) end
local function safename(path, name) -- generates foo.bar, foo[3], or foo['b a r']
local n = name == nil and '' or name
local plain = type(n) == "string" and n:match("^[%l%u_][%w_]*$") and not keyword[n]
local safe = plain and n or '['..safestr(n)..']'
return (path or '')..(plain and path and '.' or '')..safe, safe end
local alphanumsort = type(opts.sortkeys) == 'function' and opts.sortkeys or function(k, o, n) -- k=keys, o=originaltable, n=padding
local maxn, to = tonumber(n) or 12, {number = 'a', string = 'b'}
local function padnum(d) return ("%0"..tostring(maxn).."d"):format(tonumber(d)) end
table.sort(k, function(a,b)
-- sort numeric keys first: k[key] is not nil for numerical keys
return (k[a] ~= nil and 0 or to[type(a)] or 'z')..(tostring(a):gsub("%d+",padnum))
< (k[b] ~= nil and 0 or to[type(b)] or 'z')..(tostring(b):gsub("%d+",padnum)) end) end
local function val2str(t, name, indent, insref, path, plainindex, level)
local ttype, level = type(t), (level or 0)
local spath, sname = safename(path, name)
local tag = plainindex and
((type(name) == "number") and '' or name..space..'='..space) or
(name ~= nil and sname..space..'='..space or '')
if seen[t] then -- already seen this element
sref[#sref+1] = spath..space..'='..space..seen[t]
return tag..'nil'..comment('ref', level) end
-- protect from those cases where __tostring may fail
if metatostring ~= false then
local mt = getmetatable(t)
if type(mt) == 'table' then
local to, tr = pcall(function() return mt.__tostring(t) end)
local so, sr = pcall(function() return mt.__serialize(t) end)
if (to or so) then -- knows how to serialize itself
seen[t] = insref or spath
t = so and sr or tr
ttype = type(t)
end -- new value falls through to be serialized
end
end
if ttype == "table" then
if level >= maxl then return tag..'{}'..comment('maxlvl', level) end
seen[t] = insref or spath
if next(t) == nil then return tag..'{}'..comment(t, level) end -- table empty
if maxlen and maxlen < 0 then return tag..'{}'..comment('maxlen', level) end
local maxn, o, out = math.min(#t, maxnum or #t), {}, {}
for key = 1, maxn do o[key] = key end
if not maxnum or #o < maxnum then
local n = #o -- n = n + 1; o[n] is much faster than o[#o+1] on large tables
for key in pairs(t) do if o[key] ~= key then n = n + 1; o[n] = key end end end
if maxnum and #o > maxnum then o[maxnum+1] = nil end
if opts.sortkeys and #o > maxn then alphanumsort(o, t, opts.sortkeys) end
local sparse = sparse and #o > maxn -- disable sparsness if only numeric keys (shorter output)
for n, key in ipairs(o) do
local value, ktype, plainindex = t[key], type(key), n <= maxn and not sparse
if opts.valignore and opts.valignore[value] -- skip ignored values; do nothing
or opts.keyallow and not opts.keyallow[key]
or opts.keyignore and opts.keyignore[key]
or opts.valtypeignore and opts.valtypeignore[type(value)] -- skipping ignored value types
or sparse and value == nil then -- skipping nils; do nothing
elseif ktype == 'table' or ktype == 'function' or badtype[ktype] then
if not seen[key] and not globals[key] then
sref[#sref+1] = 'placeholder'
local sname = safename(iname, gensym(key)) -- iname is table for local variables
sref[#sref] = val2str(key,sname,indent,sname,iname,true) end
sref[#sref+1] = 'placeholder'
local path = seen[t]..'['..tostring(seen[key] or globals[key] or gensym(key))..']'
sref[#sref] = path..space..'='..space..tostring(seen[value] or val2str(value,nil,indent,path))
else
out[#out+1] = val2str(value,key,indent,insref,seen[t],plainindex,level+1)
if maxlen then
maxlen = maxlen - #out[#out]
if maxlen < 0 then break end
end
end
end
local prefix = string.rep(indent or '', level)
local head = indent and '{\n'..prefix..indent or '{'
local body = table.concat(out, ','..(indent and '\n'..prefix..indent or space))
local tail = indent and "\n"..prefix..'}' or '}'
return (custom and custom(tag,head,body,tail,level) or tag..head..body..tail)..comment(t, level)
elseif badtype[ttype] then
seen[t] = insref or spath
return tag..globerr(t, level)
elseif ttype == 'function' then
seen[t] = insref or spath
if opts.nocode then return tag.."function() --[[..skipped..]] end"..comment(t, level) end
local ok, res = pcall(string.dump, t)
local func = ok and "((loadstring or load)("..safestr(res)..",'@serialized'))"..comment(t, level)
return tag..(func or globerr(t, level))
else return tag..safestr(t) end -- handle all other types
end
local sepr = indent and "\n" or ";"..space
local body = val2str(t, name, indent) -- this call also populates sref
local tail = #sref>1 and table.concat(sref, sepr)..sepr or ''
local warn = opts.comment and #sref>1 and space.."--[[incomplete output with shared/self-references skipped]]" or ''
return not name and body..warn or "do local "..body..sepr..tail.."return "..name..sepr.."end"
end
local function deserialize(data, opts)
local env = (opts and opts.safe == false) and G
or setmetatable({}, {
__index = function(t,k) return t end,
__call = function(t,...) error("cannot call functions") end
})
local f, res = (loadstring or load)('return '..data, nil, nil, env)
if not f then f, res = (loadstring or load)(data, nil, nil, env) end
if not f then return f, res end
if setfenv then setfenv(f, env) end
return pcall(f)
end
local function merge(a, b) if b then for k,v in pairs(b) do a[k] = v end end; return a; end
return { _NAME = n, _COPYRIGHT = c, _DESCRIPTION = d, _VERSION = v, serialize = s,
load = deserialize,
dump = function(a, opts) return s(a, merge({name = '_', compact = true, sparse = true, metatostring = false}, opts)) end,
line = function(a, opts) return s(a, merge({sortkeys = true, comment = false, metatostring = false}, opts)) end,
block = function(a, opts) return s(a, merge({indent = ' ', sortkeys = true, comment = false, metatostring = false}, opts)) end }
end

384
stdlib_1.4.8/stdlib/vendor/version.lua vendored Normal file
View File

@@ -0,0 +1,384 @@
---
-- Version comparison library for Lua.
--
-- Comparison is simple and straightforward, with basic support for SemVer.
--
-- @usage
-- local version = require("version")
--
-- -- create a version and perform some comparisons
-- local v = version("3.1.0")
-- assert( v == version("3.1")) -- missing elements default to zero, and hence are equal
-- assert( v > version("3.0"))
--
-- -- create a version range, and check whether a version is within that range
-- local r = version.range("2.75", "3.50.3")
-- assert(r:matches(v))
--
-- -- create a set of multiple ranges, adding elements in a chained fashion
-- local compatible = version.set("1.1","1.2"):allowed("2.1", "2.5"):disallowed("2.3")
--
-- assert(compatible:matches("1.1.3"))
-- assert(compatible:matches("1.1.3"))
-- assert(compatible:matches("2.4"))
-- assert(not compatible:matches("2.0"))
-- assert(not compatible:matches("2.3"))
--
-- -- print a formatted set
-- print(compatible) --> "1.1 to 1.2 and 2.1 to 2.5, but not 2.3"
--
-- -- create an upwards compatibility check, allowing all versions 1.x
-- local c = version.set("1.0","2.0"):disallowed("2.0")
-- assert(c:matches("1.4"))
-- assert(not c:matches("2.0"))
--
-- -- default parsing
-- print(version("5.2")) -- "5.2"
-- print(version("Lua 5.2 for me")) -- "5.2"
-- print(version("5..2")) -- nil, "Not a valid version element: '5..2'"
--
-- -- strict parsing
-- print(version.strict("5.2")) -- "5.2"
-- print(version.strict("Lua 5.2 for me")) -- nil, "Not a valid version element: 'Lua 5.2 for me'"
-- print(version.strict("5..2")) -- nil, "Not a valid version element: '5..2'"
--
-- @copyright Kong Inc.
-- @author Thijs Schreijer
-- @license Apache 2.0
local table_insert = table.insert
local table_concat = table.concat
local math_max = math.max
-- Utility split function
local function split(str, pat)
local t = {}
local fpat = "(.-)" .. pat
local last_end = 1
local s, e, cap = str:find(fpat, 1)
while s do
if s ~= 1 or cap ~= "" then
table_insert(t,cap)
end
last_end = e + 1
s, e, cap = str:find(fpat, last_end)
end
if last_end <= #str then
cap = str:sub(last_end)
table_insert(t, cap)
end
return t
end
-- foreward declaration of constructor
local _new, _range, _set
-- Metatables for version, range and set
local mt_version
mt_version = {
__index = {
--- Matches a provider-version on a consumer-version based on the
-- semantic versioning specification.
-- The implementation does not support pre-release and/or build metadata,
-- only the major, minor, and patch levels are compared.
-- @function ver:semver
-- @param v Version (string or `version` object) as served by the provider
-- @return `true` or `false` whether the version matches, or `nil+err`
-- @usage local consumer = "1.2" -- consumer requested version
-- local provider = "1.5.2" -- provider served version
--
-- local compatible = version(consumer):semver(provider)
semver = function(self, v)
-- this function will be called once (in the meta table), it will set
-- the actual function on the version table itself
if self[1] == 0 then
-- major 0 is only compatible when equal
self.semver = function(self2, v2)
if getmetatable(v2) ~= mt_version then
local parsed, err = _new(v2, self2.strict)
if not parsed then return nil, err end
v2 = parsed
end
return self2 == v2
end
elseif self[4] then
-- more than 3 elements, cannot compare
self.semver = function()
return nil, "Version has too many elements (semver max 3)"
end
else
local semver_set = _set(self, self[1] + 1, self.strict):disallowed(self[1] + 1)
self.semver = function(_, v2)
return semver_set:matches(v2)
end
end
return self:semver(v)
end
},
__eq = function(a,b)
local l = math_max(#a, #b)
for i = 1, l do
if (a[i] or 0) ~= (b[i] or 0) then
return false
end
end
return true
end,
__lt = function(a,b)
if getmetatable(a) ~= mt_version or getmetatable(b) ~= mt_version then
local t = getmetatable(a) ~= mt_version and type(a) or type(b)
error("cannot compare a 'version' to a '" .. t .. "'", 2)
end
local l = math_max(#a, #b)
for i = 1, l do
if (a[i] or 0) < (b[i] or 0) then
return true
end
if (a[i] or 0) > (b[i] or 0) then
return false
end
end
return false
end,
__tostring = function(self)
return table_concat(self, ".")
end,
}
local mt_range = {
__index = {
--- Matches a version on a range.
-- @function range:matches
-- @param v Version (string or `version` object) to match
-- @return `true` or `false` whether the version matches the range, or `nil+err`
matches = function(self, v)
if getmetatable(v) ~= mt_version then
local parsed, err = _new(v, self.strict)
if not parsed then return nil, err end
v = parsed
end
return (v >= self.from) and (v <= self.to)
end,
},
__tostring = function(self)
local f, t = tostring(self.from), tostring(self.to)
if f == t then
return f
else
return f .. " to " .. t
end
end,
}
local mt_set = {
__index = {
--- Adds an ALLOWED range to the set.
-- @function set:allowed
-- @param v1 Version or range, if version, the FROM version in either string or `version` object format
-- @param v2 Version (optional), TO version in either string or `version` object format
-- @return The `set` object, to easy chain multiple allowed/disallowed ranges, or `nil+err`
allowed = function(self, v1, v2)
if getmetatable(v1) == mt_range then
assert (v2 == nil, "First parameter was a range, second must be nil.")
table_insert(self.ok, v1)
else
local r, err = _range(v1, v2, self.strict)
if not r then return nil, err end
table_insert(self.ok, r)
end
return self
end,
--- Adds a DISALLOWED range to the set.
-- @function set:disallowed
-- @param v1 Version or range, if version, the FROM version in either string or `version` object format
-- @param v2 Version (optional), TO version in either string or `version` object format
-- @return The `set` object, to easy chain multiple allowed/disallowed ranges, or `nil+err`
disallowed = function(self,v1, v2)
if getmetatable(v1) == mt_range then
assert (v2 == nil, "First parameter was a range, second must be nil.")
table_insert(self.nok, v1)
else
local r, err = _range(v1, v2, self.strict)
if not r then return nil, err end
table_insert(self.nok, r)
end
return self
end,
--- Matches a version against the set of allowed and disallowed versions.
--
-- NOTE: `disallowed` has a higher precedence, so a version that matches the `allowed` set,
-- but also the `disallowed` set, will return `false`.
-- @function set:matches
-- @param v1 Version to match (either string or `version` object).
-- @return `true` or `false` whether the version matches the set, or `nil+err`
matches = function(self, v)
if getmetatable(v) ~= mt_version then
local parsed, err = _new(v, self.strict)
if not parsed then return nil, err end
v = parsed
end
local success
for _, range in pairs(self.ok) do
if range:matches(v) then
success = true
break
end
end
if not success then
return false
end
for _, range in pairs(self.nok) do
if range:matches(v) then
return false
end
end
return true
end,
},
__tostring = function(self)
local ok, nok
if #self.ok == 1 then
ok = tostring(self.ok[1])
elseif #self.ok > 1 then
ok = tostring(self.ok[1])
for i = 2, #self.ok - 1 do
ok = ok .. ", " ..tostring(self.ok[i])
end
ok = ok .. ", and " .. tostring(self.ok[#self.ok])
end
if #self.nok == 1 then
nok = tostring(self.nok[1])
elseif #self.nok > 1 then
nok = tostring(self.nok[1])
for i = 2, #self.nok - 1 do
nok = nok .. ", " ..tostring(self.nok[i])
end
nok = nok .. ", and " .. tostring(self.nok[#self.nok])
end
if ok and nok then
return ok .. ", but not " .. nok
else
return ok
end
end,
}
_new = function(v, strict)
v = tostring(v)
if strict then
-- edge case: do not allow trailing dot
if v:sub(-1,-1) == "." then
return nil, "Not a valid version element: '"..tostring(v).."'"
end
else
local m = v:match("(%d[%d%.]*)")
if not m then
return nil, "Not a valid version element: '"..tostring(v).."'"
end
v = m
end
local t = split(v, "%.")
for i, s in ipairs(t) do
local n = tonumber(s)
if not n then
return nil, "Not a valid version element: '"..tostring(v).."'"
end
t[i] = n
end
t.strict = strict
return setmetatable(t, mt_version)
end
_range = function(v1,v2, strict)
local err
assert (v1 or v2, "At least one parameter is required")
v1 = v1 or "0"
v2 = v2 or v1
if getmetatable(v1) ~= mt_version then
v1, err = _new(v1, strict)
if not v1 then return nil, err end
end
if getmetatable(v2) ~= mt_version then
v2, err = _new(v2, strict)
if not v2 then return nil, err end
end
if v1 > v2 then
return nil, "FROM version must be less than or equal to the TO version"
end
return setmetatable({
from = v1,
to = v2,
strict = strict,
}, mt_range)
end
_set = function(v1, v2, strict)
return setmetatable({
ok = {},
nok = {},
strict = strict,
}, mt_set):allowed(v1, v2)
end
local make_module = function(strict)
return setmetatable({
--- Creates a new version object from a string.
-- The returned table will have
-- comparison operators, eg. LT, EQ, GT. For all comparisons, any missing numbers
-- will be assumed to be "0" on the least significant side of the version string.
--
-- Calling on the module table is a shortcut to `new`.
-- @param v String formatted as numbers separated by dots (no limit on number of elements).
-- @return `version` object, or `nil+err`
-- @usage local v = version.new("0.1")
-- -- is identical to
-- local v = version("0.1")
--
-- print(v) --> "0.1"
-- print(v[1]) --> 0
-- print(v[2]) --> 1
new = function(v) return _new(v, strict) end,
--- Creates a version range. A `range` object represents a range of versions.
-- @param v1 The FROM version of the range (string or `version` object). If `nil`, assumed to be 0.
-- @param v2 (optional) The TO version of the range (string or `version` object). Defaults to `v1`.
-- @return range object with `from` and `to` fields and `set:matches` method, or `nil+err`.
-- @usage local r = version.range("0.1"," 2.4")
--
-- print(v.from) --> "0.1"
-- print(v.to[1]) --> 2
-- print(v.to[2]) --> 4
range = function(v1,v2) return _range(v1, v2, strict) end,
--- Creates a version set.
-- A `set` is an object that contains a number of allowed and disallowed version `range` objects.
-- @param v1 initial version/range to allow, see `set:allowed` for parameter descriptions
-- @param v2 initial version/range to allow, see `set:allowed` for parameter descriptions
-- @return a `set` object, with `ok` and `nok` lists and a `set:matches` method, or `nil+err`
set = function(v1, v2) return _set(v1, v2, strict) end,
}, {
__call = function(self, ...)
return self.new(...)
end
})
end
local _M = make_module(false)
--- Similar module, but with stricter parsing rules.
-- `version.strict` is identical to the `version` module itself, but it requires
-- exact version strings, where as the regular parser will simply grab the
-- first sequence of numbers and dots from the string.
-- @field strict same module, but for stricter parsing.
_M.strict = make_module(true)
return _M