diff --git a/.luarc.json b/.luarc.json deleted file mode 100644 index d672eb8..0000000 --- a/.luarc.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "$schema": "https://raw.githubusercontent.com/sumneko/vscode-lua/master/setting/schema.json", - "Lua.diagnostics.disable": [ - "lowercase-global" - ], - "Lua.diagnostics.globals": [ - "mods", - "table_size", - "log", - "localised_print", - "serpent", - "global", - "__DebugAdapter", - "__Profiler", - "create_inactivity_order" - ], - "Lua.runtime.builtin": { - "coroutine": "disable", - "debug": "disable", - "io": "disable", - "math": "disable", - "os": "disable", - "package": "disable" - }, - "Lua.runtime.plugin": "/home/mami/.config/Code/User/workspaceStorage/9536dbf0665a54126a4b0958ecd5829f/justarandomgeek.factoriomod-debug/sumneko-3rd/factorio/plugin.lua", - "Lua.runtime.special": { - "__object_name": "type" - }, - "Lua.workspace.checkThirdParty": false, - "Lua.workspace.library": [ - "~/.steam/steam/steamapps/common/Factorio/data", - "~/.steam/steam/steamapps/common/Factorio/data/core/lualib", - "./.vscode/factorio", - "/home/mami/.config/Code/User/workspaceStorage/9536dbf0665a54126a4b0958ecd5829f/justarandomgeek.factoriomod-debug/sumneko-3rd/factorio/library" - ], - "runtime": { - "plugin": "C:\\Users\\mmoni\\files\\data\\projects\\factorio\\cybersyn\\.vscode\\lua\\plugin.lua", - "pluginArgs": [ - "--global-as-class", - "--mode=mods", - "--mod_name=ltndless" - ] - }, - "runtime.path": [ - "?.lua", - "?/init.lua", - "src/?.lua", - "src/scripts/?.lua", - "src/?/init.lua", - "src/scripts/?/init.lua" - ], - "runtime.pathStrict": false, - "workspace": { - "useGitIgnore": false - }, - "workspace.ignoreSubmodules": false -} \ No newline at end of file diff --git a/.vscode/flib/LICENSE b/.vscode/flib/LICENSE new file mode 100644 index 0000000..ba4cee1 --- /dev/null +++ b/.vscode/flib/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 raiguard + +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. diff --git a/.vscode/flib/README.md b/.vscode/flib/README.md new file mode 100644 index 0000000..02fd482 --- /dev/null +++ b/.vscode/flib/README.md @@ -0,0 +1,24 @@ +# Factorio Library +The Factorio Library is a set of high-quality, commonly-used utilities for +creating Factorio mods. + +## Usage + +Download the latest release from the +[mod portal](https://mods.factorio.com/mod/flib) unzip it, and put it in your +mods directory. You can access libraries provided by flib with +`require("__flib__/event")`, etc. + +Add the flib directory to your language server's library and install the +[Factorio LSP plugin](https://github.com/JanSharp/FactorioSumnekoLuaPlugin) to +enable autocomplete and in-line documentation of flib functions. + +You can view the legacy online documentation +[here](https://factoriolib.github.io/flib/index.html). Please note that this +documentation is out-of-date, and will be replaced with a new site in the +future. + +## Contributing + +Please use the [factorio-mods-devel](https://lists.sr.ht/~raiguard/factorio-mods-devel) +mailing list for support, suggestions, and/or patches. diff --git a/.vscode/flib/area.lua b/.vscode/flib/area.lua new file mode 100644 index 0000000..c45429c --- /dev/null +++ b/.vscode/flib/area.lua @@ -0,0 +1,359 @@ +--- @diagnostic disable +--- @deprecated Use `bounding-box` instead. +local flib_area = {} + +--- @deprecated Use `bounding-box` instead. +function flib_area.ceil(self) + if not self.left_top then + self = flib_area.from_shorthand(self) + end + self.left_top = { + x = math.floor(self.left_top.x), + y = math.floor(self.left_top.y), + } + self.right_bottom = { + x = math.ceil(self.right_bottom.x), + y = math.ceil(self.right_bottom.y), + } + + return self +end + +--- @deprecated Use `bounding-box` instead. +function flib_area.center(self) + if not self.left_top then + self = flib_area.from_shorthand(self) + end + return { + x = self.left_top.x + (flib_area.width(self) / 2), + y = self.left_top.y + (flib_area.height(self) / 2), + } +end + +--- @deprecated Use `bounding-box` instead. +function flib_area.center_on(self, center_point) + if not self.left_top then + self = flib_area.from_shorthand(self) + end + + local height = flib_area.height(self) + local width = flib_area.width(self) + + self.left_top = { + x = center_point.x - (width / 2), + y = center_point.y - (height / 2), + } + self.right_bottom = { + x = center_point.x + (width / 2), + y = center_point.y + (height / 2), + } + + return self +end + +--- @deprecated Use `bounding-box` instead. +function flib_area.contains_area(self, other_area) + if not self.left_top then + self = flib_area.from_shorthand(self) + end + + return ( + self.left_top.x <= other_area.left_top.x + and self.left_top.y <= other_area.left_top.y + and self.right_bottom.x >= other_area.right_bottom.x + and self.right_bottom.y >= other_area.right_bottom.y + ) +end + +--- @deprecated Use `bounding-box` instead. +function flib_area.contains_position(self, position) + if not self.left_top then + self = flib_area.from_shorthand(self) + end + + return ( + self.left_top.x <= position.x + and self.right_bottom.x >= position.x + and self.left_top.y <= position.y + and self.right_bottom.y >= position.y + ) +end + +--- @deprecated Use `bounding-box` instead. +function flib_area.corners(self) + if not self.left_top then + self = flib_area.from_shorthand(self) + end + + self.left_bottom = { + x = self.left_top.x, + y = self.right_bottom.y, + } + self.right_top = { + x = self.right_bottom.x, + y = self.left_top.y, + } + + return self +end + +--- @deprecated Use `bounding-box` instead. +function flib_area.distance_to_nearest_edge(self, position) + if not self.left_top then + self = flib_area.from_shorthand(self) + end + + local x_distance = math.min(math.abs(self.left_top.x - position.x), math.abs(self.right_bottom.x - position.x)) + local y_distance = math.min(math.abs(self.left_top.y - position.y), math.abs(self.right_bottom.y - position.y)) + + return math.min(x_distance, y_distance) +end + +--- @deprecated Use `bounding-box` instead. +function flib_area.expand(self, delta) + if not self.left_top then + self = flib_area.from_shorthand(self) + end + + self.left_top.x = self.left_top.x - delta + self.right_bottom.x = self.right_bottom.x + delta + self.left_top.y = self.left_top.y - delta + self.right_bottom.y = self.right_bottom.y + delta + + return self +end + +--- @deprecated Use `bounding-box` instead. +function flib_area.expand_to_contain_area(self, other_area) + if not self.left_top then + self = flib_area.from_shorthand(self) + end + + self.left_top = { + x = self.left_top.x < other_area.left_top.x and self.left_top.x or other_area.left_top.x, + y = self.left_top.y < other_area.left_top.y and self.left_top.y or other_area.left_top.y, + } + self.right_bottom = { + x = self.right_bottom.x > other_area.right_bottom.x and self.right_bottom.x or other_area.right_bottom.x, + y = self.right_bottom.y > other_area.right_bottom.y and self.right_bottom.y or other_area.right_bottom.y, + } + + return self +end + +--- @deprecated Use `bounding-box` instead. +function flib_area.expand_to_contain_position(self, position) + if not self.left_top then + self = flib_area.from_shorthand(self) + end + + self.left_top = { + x = self.left_top.x < position.x and self.left_top.x or position.x, + y = self.left_top.y < position.y and self.left_top.y or position.y, + } + self.right_bottom = { + x = self.right_bottom.x > position.x and self.right_bottom.x or position.x, + y = self.right_bottom.y > position.y and self.right_bottom.y or position.y, + } + + return self +end + +--- @deprecated Use `bounding-box` instead. +function flib_area.floor(self) + if not self.left_top then + self = flib_area.from_shorthand(self) + end + + self.left_top = { + x = math.ceil(self.left_top.x), + y = math.ceil(self.left_top.y), + } + self.right_bottom = { + x = math.floor(self.right_bottom.x), + y = math.floor(self.right_bottom.y), + } + + return self +end + +--- @deprecated Use `bounding-box` instead. +function flib_area.from_dimensions(dimensions, center) + center = center or { x = 0, y = 0 } + local self = { + left_top = { + x = center.x - (dimensions.width / 2), + y = center.y - (dimensions.height / 2), + }, + right_bottom = { + x = center.x + (dimensions.width / 2), + y = center.y + (dimensions.height / 2), + }, + } + flib_area.load(self) + return self +end + +--- @deprecated Use `bounding-box` instead. +function flib_area.from_position(position, snap) + local self + if snap then + local floored_position = { x = math.floor(position.x), y = math.floor(position.y) } + self = { + left_top = { x = floored_position.x, y = floored_position.y }, + right_bottom = { x = floored_position.x + 1, y = floored_position.y + 1 }, + } + else + self = { + left_top = { x = position.x - 0.5, y = position.y - 0.5 }, + right_bottom = { x = position.x + 0.5, y = position.y + 0.5 }, + } + end + + if self then + flib_area.load(self) + return self + end +end + +--- @deprecated Use `bounding-box` instead. +function flib_area.from_shorthand(area) + local self = { + left_top = { x = area[1][1], y = area[1][2] }, + right_bottom = { x = area[2][1], y = area[2][2] }, + } + flib_area.load(self) + return self +end + +--- @deprecated Use `bounding-box` instead. +function flib_area.height(self) + if not self.left_top then + self = flib_area.from_shorthand(self) + end + + return math.abs(self.right_bottom.y - self.left_top.y) +end + +--- @deprecated Use `bounding-box` instead. +function flib_area.iterate(self, step, starting_offset) + starting_offset = starting_offset or { x = 0, y = 0 } + if not self.left_top then + self = flib_area.from_shorthand(self) + end + + step = step or 1 + + local x = self.left_top.x + starting_offset.x + local y = self.left_top.y + starting_offset.y + local max_x = self.right_bottom.x + local max_y = self.right_bottom.y + local first = true + + return function() + if first then + first = false + return { x = x, y = y } + end + + local new_x = x + step + if x < max_x and new_x < max_x then + x = new_x + else + local new_y = y + step + if y < max_y and new_y < max_y then + x = self.left_top.x + starting_offset.x + y = new_y + else + return nil + end + end + + return { x = x, y = y } + end +end + +--- @deprecated Use `bounding-box` instead. +function flib_area.load(area) + if not area.left_top then + area = flib_area.from_shorthand(area) + end + return setmetatable(area, { __index = flib_area }) +end + +--- @deprecated Use `bounding-box` instead. +function flib_area.move(self, delta) + if not self.left_top then + self = flib_area.from_shorthand(self) + end + + self.left_top.x = self.left_top.x + delta.x + self.left_top.y = self.left_top.y + delta.y + self.right_bottom.x = self.right_bottom.x + delta.x + self.right_bottom.y = self.right_bottom.y + delta.y + + return self +end + +--- @deprecated Use `bounding-box` instead. +function flib_area.rotate(self) + -- save current properties + local center = flib_area.center(self) + local height = flib_area.height(self) + local width = flib_area.width(self) + + local radius_x = height / 2 + local radius_y = width / 2 + + self.left_top.x = center.x - radius_x + self.right_bottom.x = center.x + radius_x + + self.left_top.y = center.y - radius_y + self.right_bottom.y = center.y + radius_y + + return self +end + +--- @deprecated Use `bounding-box` instead. +function flib_area.square(self) + local radius = math.max(flib_area.height(self), flib_area.width(self)) + + return flib_area.from_dimensions({ height = radius, width = radius }, flib_area.center(self)) +end + +--- @deprecated Use `bounding-box` instead. +function flib_area.strip(self) + return { + left_top = { + x = self.left_top.x, + y = self.left_top.y, + }, + right_bottom = { + x = self.right_bottom.x, + y = self.right_bottom.y, + }, + } +end + +--- @deprecated Use `bounding-box` instead. +function flib_area.to_shorthand(self) + if not self.left_top then + return self + end + + return { + { self.left_top.x, self.left_top.y }, + { self.right_bottom.x, self.right_bottom.y }, + } +end + +--- @deprecated Use `bounding-box` instead. +function flib_area.width(self) + if not self.left_top then + self = flib_area.from_shorthand(self) + end + + return math.abs(self.right_bottom.x - self.left_top.x) +end + +return flib_area diff --git a/.vscode/flib/bounding-box.lua b/.vscode/flib/bounding-box.lua new file mode 100644 index 0000000..da8b998 --- /dev/null +++ b/.vscode/flib/bounding-box.lua @@ -0,0 +1,329 @@ +local position = require("__flib__/position") + +--- Utilities for manipulating bounding boxes. All functions support both the shorthand and explicit syntaxes for boxes +--- and positions, and will preserve the syntax that was passed in. Boxes are considered immutable; all functions will +--- return new boxes. +--- @class flib_bounding_box +local flib_bounding_box = {} + +--- Return a new box expanded to the nearest tile edges. +--- @param box BoundingBox +--- @return BoundingBox +function flib_bounding_box.ceil(box) + if box.left_top then + return { + left_top = { x = math.floor(box.left_top.x), y = math.floor(box.left_top.y) }, + right_bottom = { x = math.ceil(box.right_bottom.x), y = math.ceil(box.right_bottom.y) }, + } + else + return { + { math.floor(box[1][1]), math.floor(box[1][2]) }, + { math.ceil(box[2][1]), math.ceil(box[2][2]) }, + } + end +end + +--- Calculate the centerpoint of the box. +--- @param box BoundingBox +--- @return MapPosition +function flib_bounding_box.center(box) + if box.left_top then + return { + x = (box.left_top.x + box.right_bottom.x) / 2, + y = (box.left_top.y + box.right_bottom.y) / 2, + } + else + return { + (box[1][1] + box[2][1]) / 2, + (box[1][2] + box[2][2]) / 2, + } + end +end + +--- Check if the first box contains the second box. +--- @param box1 BoundingBox +--- @param box2 BoundingBox +--- @return boolean +function flib_bounding_box.contains_box(box1, box2) + local box1 = flib_bounding_box.ensure_explicit(box1) + local box2 = flib_bounding_box.ensure_explicit(box2) + + return box1.left_top.x <= box2.left_top.x + and box1.left_top.y <= box2.left_top.y + and box1.right_bottom.x >= box2.right_bottom.x + and box1.right_bottom.y >= box2.right_bottom.y +end + +--- Check if the given box contains the given position. +--- @param box BoundingBox +--- @param pos MapPosition +--- @return boolean +function flib_bounding_box.contains_position(box, pos) + local box = flib_bounding_box.ensure_explicit(box) + local pos = position.ensure_explicit(pos) + return + box.left_top.x <= pos.x and box.left_top.y <= pos.y and box.right_bottom.x >= pos.x and box.right_bottom.y >= pos.y +end + +--- Return the box in explicit form. +--- @param box BoundingBox +--- @return BoundingBox +function flib_bounding_box.ensure_explicit(box) + return { + left_top = position.ensure_explicit(box.left_top or box[1]), + right_bottom = position.ensure_explicit(box.right_bottom or box[2]), + } +end + +--- Return the box in shorthand form. +--- @param box BoundingBox +--- @return BoundingBox +function flib_bounding_box.ensure_short(box) + return { + position.ensure_short(box.left_top or box[1]), + position.ensure_short(box.right_bottom or box[2]), + } +end + +--- Return a new box with initial dimensions box1, expanded to contain box2. +--- @param box1 BoundingBox +--- @param box2 BoundingBox +--- @return BoundingBox +function flib_bounding_box.expand_to_contain_box(box1, box2) + local box2 = flib_bounding_box.ensure_explicit(box2) + + if box1.left_top then + return { + left_top = { + x = math.min(box1.left_top.x, box2.left_top.x), + y = math.min(box1.left_top.y, box2.left_top.y), + }, + right_bottom = { + x = math.max(box1.right_bottom.x, box2.right_bottom.x), + y = math.max(box1.right_bottom.y, box2.right_bottom.y), + }, + } + else + return { + { + math.min(box1[1][1], box2.left_top.x), + math.min(box1[1][2], box2.left_top.y), + }, + { + math.max(box1[2][1], box2.right_bottom.x), + math.max(box1[2][2], box2.right_bottom.y), + }, + } + end +end + +--- Return a new box expanded to contain the given position. +--- @param box BoundingBox +--- @param pos MapPosition +--- @return BoundingBox +function flib_bounding_box.expand_to_contain_position(box, pos) + local pos = position.ensure_explicit(pos) + + if box.left_top then + return { + left_top = { x = math.min(box.left_top.x, pos.x), y = math.min(box.left_top.y, pos.y) }, + right_bottom = { x = math.max(box.right_bottom.x, pos.x), y = math.max(box.right_bottom.y, pos.y) }, + } + else + return { + { math.min(box[1][1], pos.x), math.min(box[1][2], pos.y) }, + { math.max(box[2][1], pos.x), math.max(box[2][2], pos.y) }, + } + end +end + +--- Return a new box shrunk to the nearest tile edges. +--- @param box BoundingBox +--- @return BoundingBox +function flib_bounding_box.floor(box) + if box.left_top then + return { + left_top = { x = math.ceil(box.left_top.x), y = math.ceil(box.left_top.y) }, + right_bottom = { x = math.floor(box.right_bottom.x), y = math.floor(box.right_bottom.y) }, + } + else + return { + { math.ceil(box[1][1]), math.ceil(box[1][2]) }, + { math.floor(box[2][1]), math.floor(box[2][2]) }, + } + end +end + +--- Create a new box from a centerpoint and dimensions. +--- @param center MapPosition +--- @param width number +--- @param height number +--- @return BoundingBox +function flib_bounding_box.from_dimensions(center, width, height) + if center.x then + return { + left_top = { x = center.x - width / 2, y = center.y - height / 2 }, + right_bottom = { x = center.x - width / 2, y = center.y - height / 2 }, + } + else + return { + { center[1] - width / 2, center[2] - height / 2 }, + { center[1] - width / 2, center[2] - height / 2 }, + } + end +end + +--- Create a 1x1 box from the given position, optionally snapped to the containing tile edges. +--- @param pos MapPosition +--- @param snap boolean? +--- @return BoundingBox +function flib_bounding_box.from_position(pos, snap) + if snap then + pos = position.floor(pos) + else + pos = position.sub(pos, { 0.5, 0.5 }) + end + local x = pos.x or pos[1] + local y = pos.y or pos[2] + if pos.x then + return { + left_top = { x = x, y = y }, + right_bottom = { x = x + 1, y = y + 1 }, + } + else + return { + { x, y }, + { x + 1, y + 1 }, + } + end +end + +--- Calculate the height of the box. +--- @param box BoundingBox +--- @return number +function flib_bounding_box.height(box) + if box.left_top then + return box.right_bottom.y - box.left_top.y + else + return box[2][2] - box[1][2] + end +end + +--- Check if the first box intersects (overlaps) the second box. +--- @param box1 BoundingBox +--- @param box2 BoundingBox +--- @return boolean +function flib_bounding_box.intersects_box(box1, box2) + local box1 = flib_bounding_box.ensure_explicit(box1) + local box2 = flib_bounding_box.ensure_explicit(box2) + return + box1.left_top.x < box2.right_bottom.x + and box2.left_top.x < box1.right_bottom.x + and box1.left_top.y < box2.right_bottom.y + and box2.left_top.y < box1.right_bottom.y +end + +--- Return a new box with the same dimensions, moved by the given delta. +--- @param box BoundingBox +--- @param delta MapPosition +--- @return BoundingBox +function flib_bounding_box.move(box, delta) + local dx = delta.x or delta[1] + local dy = delta.y or delta[2] + if box.left_top then + return { + left_top = { x = box.left_top.x + dx, y = box.left_top.y + dy }, + right_bottom = { x = box.right_bottom.x + dx, y = box.right_bottom.y + dy }, + } + else + return { + { box[1][1] + dx, box[1][2] + dy }, + { box[2][1] + dx, box[2][2] + dy }, + } + end +end + +--- Return a new box with the same dimensions centered on the given position. +--- @param box BoundingBox +--- @param pos MapPosition +--- @return BoundingBox +function flib_bounding_box.recenter_on(box, pos) + local height = flib_bounding_box.height(box) + local width = flib_bounding_box.width(box) + + local pos_x = pos.x or pos[1] + local pos_y = pos.y or pos[2] + + if box.left_top then + return { + left_top = { x = pos_x - (width / 2), y = pos_y - (width / 2) }, + right_bottom = { x = pos_x + (height / 2), y = pos_y + (height / 2) }, + } + else + return { + { pos_x - (width / 2), pos_y - (width / 2) }, + { pos_x + (height / 2), pos_y + (height / 2) }, + } + end +end + +--- Return a new box grown or shrunk by the given delta. A positive delta will grow the box, a negative delta will +--- shrink it. +--- @param box BoundingBox +--- @param delta number +--- @return BoundingBox +function flib_bounding_box.resize(box, delta) + if box.left_top then + return { + left_top = { x = box.left_top.x - delta, y = box.left_top.y - delta }, + right_bottom = { x = box.right_bottom.x + delta, y = box.right_bottom.y + delta }, + } + else + return { + { box[1][1] - delta, box[1][2] - delta }, + { box[2][1] + delta, box[2][2] + delta }, + } + end +end + +--- Return a new box rotated 90 degrees about its center. +--- @param box BoundingBox +--- @return BoundingBox +function flib_bounding_box.rotate(box) + local center = flib_bounding_box.center(box) + local radius_x = flib_bounding_box.width(box) / 2 + local radius_y = flib_bounding_box.height(box) / 2 + + if box.left_top then + return { + left_top = { x = center.x - radius_y, y = center.y - radius_x }, + right_bottom = { x = center.x + radius_y, y = center.y + radius_x }, + } + else + return { + { center.x - radius_y, center.y - radius_x }, + { center.x + radius_y, center.y + radius_x }, + } + end +end + +--- Return a new box expanded to create a square. +--- @param box BoundingBox +--- @return BoundingBox +function flib_bounding_box.square(box) + local radius = math.max(flib_bounding_box.width(box), flib_bounding_box.height(box)) + return flib_bounding_box.from_dimensions(flib_bounding_box.center(box), radius, radius) +end + +--- Calculate the width of the box. +--- @param box BoundingBox +--- @return number +function flib_bounding_box.width(box) + if box.left_top then + return box.right_bottom.x - box.left_top.x + else + return box[2][1] - box[1][1] + end +end + +return flib_bounding_box diff --git a/.vscode/flib/changelog.txt b/.vscode/flib/changelog.txt new file mode 100644 index 0000000..40dc44e --- /dev/null +++ b/.vscode/flib/changelog.txt @@ -0,0 +1,319 @@ +--------------------------------------------------------------------------------------------------- +Version: 0.12.4 +Date: 2022-12-30 + Changes: + - flib_selected_frame_action_button and flib_selected_tool_button styles now have a vertical offset + - Improved slot style coloring to be more consistent + - Updated table module to use more generics for callback return values + Bugfixes: + - Fixed missing clicked graphical sets on slot styles + - Fixed missing return types in table module callback functions + - Fixed that long mod names would cut off in the dictionary-lite progress window +--------------------------------------------------------------------------------------------------- +Version: 0.12.3 +Date: 2022-12-14 + Bugfixes: + - Fixed a crash when calling `bounding_box.rotate()` + - The base mod dependency is no longer optional - it was causing the game to download flib versions that it could not run +--------------------------------------------------------------------------------------------------- +Version: 0.12.2 +Date: 2022-12-06 + Changes: + - [queue] If the queue is empty, returns nil instead of throwing an error + - [queue] Renamed functions to be much more clear, updated types to use generics + - Updated types to work with recent FMTK releases + Bugfixes: + - [dictionary-lite] Fixed an occasional crash related to Lua memory management internals +--------------------------------------------------------------------------------------------------- +Version: 0.12.1 +Date: 2022-12-06 + Bugfixes: + - [dictionary-lite] Fixed a crash when re-requesting a missed translation batch +--------------------------------------------------------------------------------------------------- +Version: 0.12.0 +Date: 2022-12-06 + Features: + - Added 'bounding-box' module + - A cleaned up version of the 'area' module that treats bounding boxes as immutable and does not use metatables + - Since the cleanup was a breaking change, a new module was created + - Added 'dictionary-lite' module + - Takes advantage of new Factorio API features to drastically simplify the process + - Increases translation speed in singleplayer by a factor of 10 + - Added 'format' module with various string formatting functions + - Added 'gui-lite' module + - Removes redundant features, simplifies the building process, and enhances type-checking and autocomplete + - Added 'migration.handle_on_configuration_changed()' + - Added 'position' module with various position manipulation functions + Changes: + - Changed preferred require syntax to '/' instead of '.' + - Deprecated 'area' module in favor of the 'bounding-box' module + - Deprecated 'dictionary' module in favor of the 'dictionary-lite' module + - Deprecated 'event' module - type checking did not work and it didn't provide much over using 'script' directly + - Deprecated 'gui' module in favor of the 'gui-lite' module + - Deprecated 'misc' module in favor of the 'format' and 'position' modules + - Removed 'queue.load()' and queue metatables + - Updated type definitions to use generics where possible + Bugfixes: + - [dictionary] Fixed translations getting stuck on Factorio 1.1.73+ +--------------------------------------------------------------------------------------------------- +Version: 0.11.2 +Date: 2022-09-18 + Bugfixes: + - [dictionary] Fixed a crash when a player left the game under certain circumstances +--------------------------------------------------------------------------------------------------- +Version: 0.11.1 +Date: 2022-09-12 + Changes: + - [dictionary] Added tooltip to GUI header explaining what it is + Bugfixes: + - [dictionary] Fixed that translations would stop being requested under certain circumstances +--------------------------------------------------------------------------------------------------- +Version: 0.11.0 +Date: 2022-09-04 + Features: + - [area] Added `area.square()` to turn the area into a square + - [area] Added `starting_offset` argument to `area.iterate()` + - [dictionary] Added a GUI that shows translation progress + - [math] Added `floored` and `ceiled` for flooring and ceiling to N decimal places. + - [math] Added `sign` to return the signedness of a number as a multiplier + - [math] Added `sum`, `maximum`, `minimum`, `midrange`, `range` for sets of numbers + Changes: + - [math] Deprecated `round_to`, `ceil_to`, `floor_to` + - [math] `clamp` defaults to clamping between 0, 1 when min or max are not set + - [dictionary] Removed string nesting to significantly reduce network traffic + - This makes translation take significantly longer + Bugfixes: + - Fixed missing annotations in math and table libraries + - [dictionary] Fixed that an empty translation would cause the translation after it to be skipped + - [migration] Fixed a crash when loading the tutorial scenario +--------------------------------------------------------------------------------------------------- +Version: 0.10.1 +Date: 2022-02-24 + Bugfixes: + - [event] Fixed a crash when passing `nil` to `event.on_nth_tick` +--------------------------------------------------------------------------------------------------- +Version: 0.10.0 +Date: 2022-02-24 + Features: + - [direction] Added `direction.from_positions()` + - [table] Added `table.get_or_insert()` + Changes: + - Converted from LDoc to EmmyLua annotations, enabling language server intellisense, but removing the docs website + - A new website or other solution for online docs will be added someday. For now, in-line documentation was decided to be more beneficial. + - [area] All constructor functions will automatically call `area.load()` to add the metatable + - [area] All functions will ensure that any passed areas have `left_top` and `right_bottom` keys, and will automatically convert ones that do not + - [migration] `migration.on_config_changed()` version migrations table is now optional +--------------------------------------------------------------------------------------------------- +Version: 0.9.2 +Date: 2021-11-18 + Bugfixes: + - Fixed the dictionary module not separating dictionaries from separate mods properly +--------------------------------------------------------------------------------------------------- +Version: 0.9.1 +Date: 2021-11-16 + Bugfixes: + - Fixed a crash when calling dictionary.load() before dictionary.init() has been able to fire + - Fixed table.slice() and table.splice() stop-n-from-end being one position off +--------------------------------------------------------------------------------------------------- +Version: 0.9.0 +Date: 2021-11-11 + Features: + - Added `queue` module + - Added `table.array_merge()` and `table.retrieve()` + - Added `flib_pin` and `flib_settings` sprites in black, white, and disabled variants + - These are for use with frame action buttons + - Added `gui.flib-keep-open` and `gui.flib-settings` locales for use with the aforementioned sprites +--------------------------------------------------------------------------------------------------- +Version: 0.8.4 +Date: 2021-09-19 + Features: + - Added `flib-dictionary-levels-per-batch` setting + Changes: + - Children may now be defined in `gui.add()` + - If you need to return refs to children, use `gui.build()` instead + - `flib-translations-per-tick` setting is now hidden +--------------------------------------------------------------------------------------------------- +Version: 0.8.3 +Date: 2021-08-20 + Bugfixes: + - Fixed that math.round() was incorrect for some negative numbers + - Fixed that the on-tick-n module was not documented and not mentioned in the v0.8.0 changelog +--------------------------------------------------------------------------------------------------- +Version: 0.8.2 +Date: 2021-08-10 + Bugfixes: + - Fixed the dictionary module not handling if the language detection string was lost across save/load +--------------------------------------------------------------------------------------------------- +Version: 0.8.1 +Date: 2021-08-09 + Changes: + - The dictionary module no longer provides an event to hook, instead returning the finished dictionaries directly from `process_translation` + - This is to allow a single-source-of-truth for the language's dictionaries, which was the original intention, but returning them in an event broke that intention +--------------------------------------------------------------------------------------------------- +Version: 0.8.0 +Date: 2021-08-08 + Features: + - Added `area.from_shorthand` and `area.to_shorthand` + - Added `dictionary` module, which is a fast and easy-to-use dictionary system for localised string translations + - Added `flib_widthless_textfield` style + - Added `gui.add`, `gui.set_action`, and `gui.get_action` + - Added `on-tick-n` module, which allows you to schedule tasks to be executed on a specific tick + - Added support for tags and actions in `gui.update` + - Children in a `GuiBuildStructure` or `GuiUpdateStructure` can now be defined in the array portion, instead of in a `children` or `tabs` table + - The subtables are still accepted for situations where they are appropriate (i.e. dynamically generated children) + - `misc.delineate_number()` now supports decimal numbers + Changes: + - `area.load` will now automatically convert from a shorthanded area to a proper area if needed + - Deprecated the old `gui` module and replaced it with the contents of `gui-beta` + - Mods can still require `gui-beta`, but it will simply redirect to `gui` instead + - Mods that depended on the old GUI module will need to update to the new one or download a copy of it to their mod - it is no longer supported whatsoever + - Deprecated the `translation` module, replaced by the new `dictionary` module + - The `translation` module still exists and can be used, but is no longer documented and its use is recommended against + Bugfixes: + - Fixed a crash when calling `gui.read_action()` when the element was invalid + - Fixed `area.from_position` not actually creating a 1x1 area +--------------------------------------------------------------------------------------------------- +Version: 0.7.0 +Date: 2021-02-12 + Features: + - Added `area` module + - Added `flib_titlebar_flow` style + - Added `divisor` argument to `math.round()`, allowing rounding to the nearest multiple of N + - Added `math.max_double` and `math.min_double` + - Added argument to `misc.ticks_to_timestring` to preserve leading zeroes + Optimizations: + - Significant performance improvements to `gui-beta.build()` + Bugfixes: + - Fixed `math.max_uint` being one number too large + - Fixed `data-util.create_icons()` not using all of the icon specifications +--------------------------------------------------------------------------------------------------- +Version: 0.6.1 +Date: 2021-01-02 + Features: + - Added proper documentation for the `gui-beta` module +--------------------------------------------------------------------------------------------------- +Version: 0.6.0 +Date: 2020-11-23 + Features: + - Added WIP `gui-beta` module + - This module is experimental, but is currently relatively stable. Use at your own risk. See the source code for documentation. + - Additional arguments may be passed to `migration.on_config_changed()` to be available in the migration functions + - Added `data-util.dark_red_button_tileset` + - Added `flib_tool_button_dark_red` button style + Changes: + - Updated to Factorio 1.1 + - Modified `flib_selected_frame_action_button` style to better match the vanilla buttons + - Made `new_layers` argument to `data-util.create_icons()` optional + - Removed deprecated require paths for `data-util` and `reverse-defines` modules +--------------------------------------------------------------------------------------------------- +Version: 0.5.0 +Date: 2020-10-22 + Features: + - Added indicator sprites + - Added `flib_indicator` image style + Changes: + - Updated thumbnail + - Changed `table.reduce()` to work with non-arrays + - Changed `misc.ticks_to_timestring()` to use `game.ticks_played` by default, instead of `game.tick` + Bugfixes: + - Fixed a crash with serialise_localised_string (credit to dbeckwith) +--------------------------------------------------------------------------------------------------- +Version: 0.4.1 +Date: 2020-09-22 + Features: + - Added a third return value to `table.for_n_of` signifying whether or not the end of the table was reached + - Added a third return value to the callback of `table.for_n_of`, which is a flag requesting an immediate iteration abort + - Added `flib_tool_button_light_green` button style + Changes: + - `table.for_n_of` now checks against the first (key) return of `next()` rather than the second (value) return + - This changes how custom `next()` functions will behave, and brings it into consistency with the rest of Lua +--------------------------------------------------------------------------------------------------- +Version: 0.4.0 +Date: 2020-09-13 + Features: + - Added `direction.to_vector_2d` + - Additional arguments passed to `migration.run` will be passed on to the version functions within the migrations table + - Added `math` module + Bugfixes: + - Fixed that using `gui.init()` to reset all filters did not update lookup tables +--------------------------------------------------------------------------------------------------- +Version: 0.3.3 +Date: 2020-08-31 + Features: + - Added a map setting to adjust translation speed (avoid multiplayer drops for those with slow internet speeds) +--------------------------------------------------------------------------------------------------- +Version: 0.3.2 +Date: 2020-08-30 + Features: + - Added `table.array_copy()` + - Added `table.partial_sort()` for sorting an array over multiple ticks + Changes: + - Passing a custom `next` function to `table.for_n_of()` will bypass the key existence check +--------------------------------------------------------------------------------------------------- +Version: 0.3.1 +Date: 2020-08-24 + Features: + - Added event.register_on_entity_destroyed() + - Added flib_naked_scroll_pane_no_padding GUI style + Changes: + - Updated to Factorio 1.0 + - Renamed data_util to data-util and reverse_defines to reverse-defines + - The old paths will still work until v0.4.0, but will print deprecation warnings + - Removed vertically_stretchable from the flib_naked_scroll_pane GUI style to fix zero-height issue in non-fixed-height containers +--------------------------------------------------------------------------------------------------- +Version: 0.3.0 +Date: 2020-08-11 + Features: + - Added "grey" and "orange" colored slot button styles + - Added two new scroll-pane GUI styles + - Added "table" module for working with tables + Changes: + - translation.init() is now required to be run during on_configuration_changed, in addition to on_init + - This is to prevent the module translating and returning strings for things that may not exist anymore +--------------------------------------------------------------------------------------------------- +Version: 0.2.0 +Date: 2020-07-26 + Features: + - Added gui.check_filter_validity(), which is a new REQUIRED setup function for the GUI module + - Added translation.is_translating(), which checks whether the given player is actively translating + - Added translation.translating_players_count(), which returns the number of players that are actively translating + - Added several button and empty-widget styles, see "styles.md" topic in docs + Changes: + - The base mod is now marked as optional, to allow compatibility with total overhaul mods + Bugfixes: + - Fixed a crash with the GUI module when a handler was removed between mod versions while being registered in global + - Fixed that gui.remove_player_filters() would not update lookup tables +--------------------------------------------------------------------------------------------------- +Version: 0.1.5 +Date: 2020-06-30 + Changes: + - Moved styles into prototypes/styles + Bugfixes: + - Fixed nonexistent sound file error by inheriting from parent #13 +--------------------------------------------------------------------------------------------------- +Version: 0.1.4 +Date: 2020-06-18 + Bugfixes: + - Fixed a crash with the translation module related to old code having incorrect variable names +--------------------------------------------------------------------------------------------------- +Version: 0.1.3 +Date: 2020-06-18 + Features: + - Added a myriad of colored slot styles; see "slot-styles.md" topic in docs +--------------------------------------------------------------------------------------------------- +Version: 0.1.2 +Date: 2020-06-10 + Bugfixes: + - get_energy_value didn't parse energy strings with no SI prefix or deka +--------------------------------------------------------------------------------------------------- +Version: 0.1.1 +Date: 2020-06-10 + Features: + - get_energy_value supports exponents from 10^-24 to 10^24 + Bugfixes: + - get_energy_value returned string, string instead of nil when unable to parse exponent +--------------------------------------------------------------------------------------------------- +Version: 0.1.0 +Date: 2020-05-24 + Features: + - Initial release diff --git a/.vscode/flib/data-util.lua b/.vscode/flib/data-util.lua new file mode 100644 index 0000000..e2fa794 --- /dev/null +++ b/.vscode/flib/data-util.lua @@ -0,0 +1,182 @@ +--- Utilities for data stage prototype manipulation. +--- @class flib_data_util +local flib_data_util = {} + +--- Copy a prototype, assigning a new name and minable properties. +--- @param prototype table +--- @param new_name string string +--- @param remove_icon? boolean +--- @return table +function flib_data_util.copy_prototype(prototype, new_name, remove_icon) + if not prototype.type or not prototype.name then + error("Invalid prototype: prototypes must have name and type properties.") + return --- @diagnostic disable-line + end + local p = table.deepcopy(prototype) + p.name = new_name + if p.minable and p.minable.result then + p.minable.result = new_name + end + if p.place_result then + p.place_result = new_name + end + if p.result then + p.result = new_name + end + if p.results then + for _, result in pairs(p.results) do + if result.name == prototype.name then + result.name = new_name + end + end + end + if remove_icon then + p.icon = nil + p.icon_size = nil + p.icon_mipmaps = nil + p.icons = nil + end + + return p +end + +--- Copy prototype.icon/icons to a new fully defined icons array, optionally adding new icon layers. +--- +--- Returns `nil` if the prototype's icons are incorrectly or incompletely defined. +--- @param prototype table +--- @param new_layers? IconSpecification[] +--- @return IconSpecification[]|nil +function flib_data_util.create_icons(prototype, new_layers) + if new_layers then + for _, new_layer in pairs(new_layers) do + if not new_layer.icon or not new_layer.icon_size then + return nil + end + end + end + + if prototype.icons then + local icons = {} + for _, v in pairs(prototype.icons) do + -- Over define as much as possible to minimize weirdness: https://forums.factorio.com/viewtopic.php?f=25&t=81980 + icons[#icons + 1] = { + icon = v.icon, + icon_size = v.icon_size or prototype.icon_size or 32, + icon_mipmaps = v.icon_mipmaps or prototype.icon_mipmaps or 0, + tint = v.tint, + scale = v.scale or 1, + shift = v.shift, + } + end + if new_layers then + for _, new_layer in pairs(new_layers) do + icons[#icons + 1] = new_layer + end + end + return icons + elseif prototype.icon then + local icons = { + { + icon = prototype.icon, + icon_size = prototype.icon_size, + icon_mipmaps = prototype.icon_mipmaps, + tint = { r = 1, g = 1, b = 1, a = 1 }, + }, + } + if new_layers then + for _, new_layer in pairs(new_layers) do + icons[#icons + 1] = new_layer + end + end + return icons + else + return nil + end +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, -- μ is invalid + ["m"] = 0.001, + ["c"] = 0.01, + ["d"] = 0.1, + [""] = 1, + ["da"] = 10, + ["h"] = 100, + ["k"] = 1000, + ["M"] = 1000000, + ["G"] = 1000000000, + ["T"] = 1000000000000, + ["P"] = 1000000000000000, + ["E"] = 1000000000000000000, + ["Z"] = 1000000000000000000000, + ["Y"] = 1000000000000000000000000, +} + +--- Convert an energy string to base unit value + suffix. +--- +--- Returns `nil` if `energy_string` is incorrectly formatted. +--- @param energy_string string +--- @return number? +--- @return string? +function flib_data_util.get_energy_value(energy_string) + if type(energy_string) == "string" then + local v, _, exp, unit = string.match(energy_string, "([%-+]?[0-9]*%.?[0-9]+)((%D*)([WJ]))") + local value = tonumber(v) + if value and exp and exponent_multipliers[exp] then + value = value * exponent_multipliers[exp] + return value, unit + end + end + return nil +end + +--- Build a sprite from constituent parts. +--- @param name? string +--- @param position? MapPosition +--- @param filename? string +--- @param size? Vector +--- @param mipmap_count? number +--- @param mods? table +--- @return SpriteSpecification +function flib_data_util.build_sprite(name, position, filename, size, mipmap_count, mods) + local def = { + type = "sprite", + name = name, + filename = filename, + position = position, + size = size, + mipmap_count = mipmap_count, + flags = { "icon" }, + } + if mods then + for k, v in pairs(mods) do + def[k] = v + end + end + return def +end + +--- An empty image. This image is 8x8 to facilitate usage with GUI styles. +flib_data_util.empty_image = "__flib__/graphics/empty.png" + +--- A black image, for use with tool backgrounds. This image is 1x1. +flib_data_util.black_image = "__flib__/graphics/black.png" + +--- A desaturated planner image. Tint this sprite to easily add your own planners. +flib_data_util.planner_base_image = "__flib__/graphics/planner.png" + +--- A dark red button tileset. Used for the `flib_tool_button_dark_red` style. +flib_data_util.dark_red_button_tileset = "__flib__/graphics/dark-red-button.png" + +return flib_data_util + +--- @class IconSpecification +--- @field icon string +--- @field icon_size int +--- @class SpriteSpecification diff --git a/.vscode/flib/data.lua b/.vscode/flib/data.lua new file mode 100644 index 0000000..6a28b8b --- /dev/null +++ b/.vscode/flib/data.lua @@ -0,0 +1,2 @@ +require("prototypes/sprite") +require("prototypes/style") diff --git a/.vscode/flib/dictionary-lite.lua b/.vscode/flib/dictionary-lite.lua new file mode 100644 index 0000000..0db3c8c --- /dev/null +++ b/.vscode/flib/dictionary-lite.lua @@ -0,0 +1,476 @@ +local gui = require("__flib__/gui-lite") +local mod_gui = require("__core__/lualib/mod-gui") +local table = require("__flib__/table") + +--- Utilities for creating dictionaries of localised string translations. +--- @class flib_dictionary +local flib_dictionary = {} + +local request_timeout_ticks = (60 * 5) + +--- @param init_only boolean? +--- @return flib_dictionary_global +local function get_data(init_only) + if not global.__flib or not global.__flib.dictionary then + error("Dictionary module was not properly initialized - ensure that all lifecycle events are handled.") + end + local data = global.__flib.dictionary + if init_only and data.init_ran then + error("Dictionaries cannot be modified after initialization.") + end + return data +end + +--- @param data flib_dictionary_global +--- @param language string +--- @return LuaPlayer? +local function get_translator(data, language) + for player_index, player_language in pairs(data.player_languages) do + if player_language == language then + local player = game.get_player(player_index) + if player and player.connected then + return player + end + end + end + -- There is no available translator, so remove this language from the pool + for player_index, player_language in pairs(data.player_languages) do + if player_language == language then + data.player_languages[player_index] = nil + end + end +end + +--- @param data flib_dictionary_global +local function update_gui(data) + local wip = data.wip + for _, player in pairs(game.players) do + local frame_flow = mod_gui.get_frame_flow(player) + local window = frame_flow.flib_translation_progress + if wip then + if not window then + _, window = gui.add(frame_flow, { + type = "frame", + name = "flib_translation_progress", + style = mod_gui.frame_style, + direction = "vertical", + { + type = "label", + style = "frame_title", + caption = { "gui.flib-translating-dictionaries" }, + tooltip = { "gui.flib-translating-dictionaries-description" }, + }, + { + type = "frame", + name = "pane", + style = "inside_shallow_frame_with_padding", + style_mods = { top_padding = 8 }, + direction = "vertical", + }, + }) + end + local pane = window.pane --[[@as LuaGuiElement]] + local mod_flow = pane[script.mod_name] + if not mod_flow then + _, mod_flow = gui.add(pane, { + type = "flow", + name = script.mod_name, + style = "centering_horizontal_flow", + style_mods = { top_margin = 4, horizontal_spacing = 8 }, + { + type = "label", + style = "caption_label", + style_mods = { minimal_width = 130 }, + caption = { "?", { "mod-name." .. script.mod_name }, script.mod_name }, + ignored_by_interaction = true, + }, + { type = "empty-widget", style = "flib_horizontal_pusher" }, + { type = "label", name = "language", style = "bold_label", ignored_by_interaction = true }, + { + type = "progressbar", + name = "bar", + style_mods = { top_margin = 1, width = 100 }, + ignored_by_interaction = true, + }, + { + type = "label", + name = "percentage", + style = "bold_label", + style_mods = { width = 24, horizontal_align = "right" }, + ignored_by_interaction = true, + }, + }) + end + local progress = wip.received_count / data.raw_count + mod_flow.language.caption = wip.language + mod_flow.bar.value = progress --[[@as double]] + mod_flow.percentage.caption = tostring(math.min(math.floor(progress * 100), 99)) .. "%" + mod_flow.tooltip = + { "", (wip.dict or { "gui.flib-finishing" }), "\n" .. wip.received_count .. " / " .. data.raw_count } + else + if window then + local mod_flow = window.pane[script.mod_name] + if mod_flow then + mod_flow.destroy() + end + if #window.pane.children == 0 then + window.destroy() + end + end + end + end +end + +--- @param data flib_dictionary_global +--- @return boolean success +local function request_next_batch(data) + local raw = data.raw + local wip = data.wip --[[@as DictWipData]] + if wip.finished then + return false + end + local requests, strings = {}, {} + for i = 1, game.is_multiplayer() and 5 or 50 do + local string + repeat + wip.key, string = next(raw[wip.dict], wip.key) + if not wip.key then + wip.dict = next(raw, wip.dict) + if not wip.dict then + -- We are done! + wip.finished = true + end + end + until string or wip.finished + if wip.finished then + break + end + requests[i] = { dict = wip.dict, key = wip.key } + strings[i] = string + end + + if #strings == 0 then + return false -- Finished + end + + local translator = wip.translator + if not translator.valid or not translator.connected then + local new_translator = get_translator(data, wip.language) + if new_translator then + wip.translator = new_translator + else + -- Cancel this translation + data.wip = nil + return false + end + end + + local ids = wip.translator.request_translations(strings) + if not ids then + return false + end + for i = 1, #ids do + wip.requests[ids[i]] = requests[i] + end + wip.request_tick = game.tick + + update_gui(data) + + return true +end + +--- @param data flib_dictionary_global +local function handle_next_language(data) + while not data.wip and #data.to_translate > 0 do + local next_language = table.remove(data.to_translate, 1) + if next_language then + local translator = get_translator(data, next_language) + if translator then + -- Start translation + local dicts = {} + local first_dict + for name in pairs(data.raw) do + first_dict = first_dict or name + dicts[name] = {} + end + -- Don't do anything if there are no dictionaries to translate + if not first_dict then + return + end + --- @class DictWipData + data.wip = { + dict = first_dict, + dicts = dicts, + finished = false, + --- @type string? + key = nil, + language = next_language, + received_count = 0, + --- @type table + requests = {}, + request_tick = 0, + translator = translator, + } + end + end + end +end + +-- Events + +flib_dictionary.on_player_dictionaries_ready = script.generate_event_name() +--- Called when a player's dictionaries are ready to be used. Handling this event is not required. +--- @class EventData.on_player_dictionaries_ready: EventData +--- @field player_index uint + +flib_dictionary.on_player_language_changed = script.generate_event_name() +--- Called when a player's language changes. Handling this event is not required. +--- @class EventData.on_player_language_changed: EventData +--- @field player_index uint +--- @field language string + +-- Lifecycle handlers + +function flib_dictionary.on_init() + -- Initialize global data + if not global.__flib then + global.__flib = {} + end + --- @class flib_dictionary_global + global.__flib.dictionary = { + init_ran = false, + --- @type table + player_languages = {}, + --- @type table + player_language_requests = {}, + --- @type table + raw = {}, + raw_count = 0, + --- @type string[] + to_translate = {}, + --- @type table> + translated = {}, + --- @type DictWipData? + wip = nil, + } + -- Initialize all existing players + for player_index, player in pairs(game.players) do + if player.connected then + flib_dictionary.on_player_joined_game({ + --- @cast player_index uint + player_index = player_index, + }) + end + end +end + +flib_dictionary.on_configuration_changed = flib_dictionary.on_init + +function flib_dictionary.on_tick() + local data = get_data() + if not data.init_ran then + data.init_ran = true + end + + -- Player language requests + for id, request in pairs(data.player_language_requests) do + if game.tick - request.tick > request_timeout_ticks then + local player = request.player + if player.valid and player.connected then + local id = player.request_translation({ "locale-identifier" }) + if id then + data.player_language_requests[id] = { + player = player, + tick = game.tick, + } + end + end + -- Deletion must be last so that the deleted entry isn't re-used for the new entry in memory + data.player_language_requests[id] = nil + end + end + + local wip = data.wip + if not wip then + return + end + + if game.tick - wip.request_tick > request_timeout_ticks then + -- next() will return the first string from the last batch because it was inserted first + local _, request = next(wip.requests) + wip.dict = request.dict + wip.finished = false + wip.key = request.key + wip.requests = {} + request_next_batch(data) + update_gui(data) + end +end + +--- @param e EventData.on_string_translated +function flib_dictionary.on_string_translated(e) + local data = get_data() + local id = e.id + + -- Player language requests + local request = data.player_language_requests[id] + if request then + data.player_language_requests[id] = nil + if not e.translated then + error("Language key request for player " .. e.player_index .. " failed") + end + if data.player_languages[e.player_index] ~= e.result then + data.player_languages[e.player_index] = e.result + script.raise_event( + flib_dictionary.on_player_language_changed, + { player_index = e.player_index, language = e.result } + ) + if data.translated[e.result] then + script.raise_event(flib_dictionary.on_player_dictionaries_ready, { player_index = e.player_index }) + return + elseif data.wip and data.wip.language == e.result then + return + elseif table.find(data.to_translate, e.result) then + return + else + table.insert(data.to_translate, e.result) + end + end + end + + handle_next_language(data) + + local wip = data.wip + if not wip then + return + end + + local request = wip.requests[id] + if request then + wip.requests[id] = nil + wip.received_count = wip.received_count + 1 + if e.translated then + wip.dicts[request.dict][request.key] = e.result + end + end + + while wip and table_size(wip.requests) == 0 and not request_next_batch(data) do + if wip.finished then + data.translated[wip.language] = wip.dicts + data.wip = nil + for player_index, language in pairs(data.player_languages) do + if wip.language == language then + script.raise_event(flib_dictionary.on_player_dictionaries_ready, { player_index = player_index }) + end + end + end + handle_next_language(data) + update_gui(data) + wip = data.wip + end +end + +--- @param e EventData.on_player_joined_game +function flib_dictionary.on_player_joined_game(e) + -- Request the player's locale identifier + local player = game.get_player(e.player_index) --[[@as LuaPlayer]] + local id = player.request_translation({ "locale-identifier" }) + if not id then + return + end + local data = get_data() + data.player_language_requests[id] = { + player = player, + tick = game.tick, + } + update_gui(data) +end + +--- Handle all non-bootstrap events with default event handlers. Will not overwrite any existing handlers. If you have +--- custom handlers for on_tick, on_string_translated, or on_player_joined_game, ensure that you call the corresponding +--- module lifecycle handler.. +function flib_dictionary.handle_events() + for id, handler in pairs({ + [defines.events.on_tick] = flib_dictionary.on_tick, + [defines.events.on_string_translated] = flib_dictionary.on_string_translated, + [defines.events.on_player_joined_game] = flib_dictionary.on_player_joined_game, + }) do + if + not script.get_event_handler(id --[[@as uint]]) + then + script.on_event(id, handler) + end + end +end + +-- Dictionary creation + +--- Create a new dictionary. The name must be unique. +--- @param name string +--- @param initial_strings Dictionary? +function flib_dictionary.new(name, initial_strings) + local raw = get_data(true).raw + if raw[name] then + error("Attempted to create dictionary '" .. name .. "' twice.") + end + raw[name] = initial_strings or {} +end + +--- Add the given string to the dictionary. +--- @param dict_name string +--- @param key string +--- @param localised LocalisedString +function flib_dictionary.add(dict_name, key, localised) + local data = get_data(true) + local raw = data.raw[dict_name] + if not raw then + error("Dictionary '" .. dict_name .. "' does not exist.") + end + if not raw[key] then + data.raw_count = data.raw_count + 1 + end + raw[key] = localised +end + +--- Get all dictionaries for the player. Will return `nil` if the player's language has not finished translating. +--- @param player_index uint +--- @return table? +function flib_dictionary.get_all(player_index) + local data = get_data() + local language = data.player_languages[player_index] + if not language then + return + end + return data.translated[language] +end + +--- Get the specified dictionary for the player. Will return `nil` if the dictionary has not finished translating. +--- @param player_index uint +--- @param dict_name string +--- @return TranslatedDictionary? +function flib_dictionary.get(player_index, dict_name) + local data = get_data() + if not data.raw[dict_name] then + error("Dictionary '" .. dict_name .. "' does not exist.") + end + local language_dicts = flib_dictionary.get_all(player_index) or {} + return language_dicts[dict_name] +end + +--- @class DictLangRequest +--- @field player LuaPlayer +--- @field tick uint + +--- @class DictTranslationRequest +--- @field language string +--- @field dict string +--- @field key string + +--- Localised strings identified by an internal key. Keys must be unique and language-agnostic. +--- @alias Dictionary table + +--- Translations are identified by their internal key. If the translation failed, then it will not be present. Locale +--- fallback groups can be used if every key needs a guaranteed translation. +--- @alias TranslatedDictionary table + +return flib_dictionary diff --git a/.vscode/flib/dictionary.lua b/.vscode/flib/dictionary.lua new file mode 100644 index 0000000..430b176 --- /dev/null +++ b/.vscode/flib/dictionary.lua @@ -0,0 +1,424 @@ +local gui = require("__flib__/gui-lite") +local mod_gui = require("__core__/lualib/mod-gui") +local table = require("__flib__/table") + +--- @diagnostic disable +--- @deprecated Use 'dictionary-lite' instead. +local flib_dictionary = {} + +local inner_separator = "" -- U+E000 +local separator = "" -- U+E001 +local translation_timeout = 600 + +-- Depending on the value of `use_local_storage`, this will be tied to `global` or will be re-generated during `on_load` +local raw = { _total_strings = 0 } + +local use_local_storage = false + +local function key_value(key, value) + return key .. inner_separator .. value .. separator +end + +local RawDictionary = {} + +--- @deprecated Use 'dictionary-lite' instead. +function RawDictionary:add(internal, translation) + local to_add = { "", internal, inner_separator, { "?", translation, "FLIB_TRANSLATION_FAILED" }, separator } + + local ref = self.ref + local i = self.batch_i + 1 + -- Due to network saturation concerns, only group five strings together + -- See https://github.com/factoriolib/flib/issues/45 + if i < 5 then + ref[i] = to_add --- @diagnostic disable-line + self.batch_i = i + else + local s_i = self.dict_i + 1 + self.dict_i = s_i + local new_set = { "", to_add } + self.ref = new_set + self.strings[s_i] = new_set + self.batch_i = 2 + end + self.total = self.total + 1 + raw._total_strings = raw._total_strings + 1 +end + +--- @class RawDictionary +function flib_dictionary.new(name, keep_untranslated, initial_contents) + if raw[name] then + error("Dictionary with the name `" .. name .. "` already exists.") + end + + --- @type LocalisedString + local initial_string = { "" } + --- @class RawDictionary + local self = { + -- Indices + batch_i = 1, + dict_i = 1, + total = 1, + -- Internal + ref = initial_string, + --- @type LocalisedString + strings = { initial_string }, + -- Meta + name = name, + } + setmetatable(self, { __index = RawDictionary }) + + for key, value in pairs(initial_contents or {}) do + self:add(key, value) + end + --- @diagnostic disable-next-line + raw[name] = { strings = self.strings, keep_untranslated = keep_untranslated } + + return self +end + +--- @deprecated Use 'dictionary-lite' instead. +function flib_dictionary.init() + if not global.__flib then + global.__flib = {} + end + global.__flib.dictionary = { + in_process = {}, + players = {}, + raw = { _total_strings = 0 }, + translated = {}, + } + if use_local_storage then + raw = { _total_strings = 0 } + else + raw = global.__flib.dictionary.raw + end +end + +--- @deprecated Use 'dictionary-lite' instead. +function flib_dictionary.load() + if not use_local_storage and global.__flib and global.__flib.dictionary then + raw = global.__flib.dictionary.raw + end +end + +--- @deprecated Use 'dictionary-lite' instead. +function flib_dictionary.translate(player) + if not player.connected then + error("Player must be connected to the game before this function can be called!") + end + + local player_data = global.__flib.dictionary.players[player.index] + if player_data then + return + end + global.__flib.dictionary.players[player.index] = { + player = player, + status = "get_language", + requested_tick = game.tick, + } + + player.request_translation({ "", "FLIB_LOCALE_IDENTIFIER", separator, { "locale-identifier" } }) +end + +local function request_translation(player_data) + local string = raw[player_data.dictionary].strings[player_data.i] + + -- We use `while` instead of `if` here just in case a dictionary doesn't have any strings in it + while not string do + local next_dictionary = next(raw, player_data.dictionary) + if next_dictionary then + -- Set the next dictionary and reset index + player_data.dictionary = next_dictionary + player_data.i = 1 + string = raw[next_dictionary].strings[1] + else + -- We're done! + player_data.status = "finished" + return + end + end + + player_data.player.request_translation({ + "", + key_value("FLIB_DICTIONARY_MOD", script.mod_name), + key_value("FLIB_DICTIONARY_NAME", player_data.dictionary), + key_value("FLIB_DICTIONARY_LANGUAGE", player_data.language), + key_value("FLIB_DICTIONARY_STRING_INDEX", player_data.i), + string, + }) + + player_data.requested_tick = game.tick +end + +--- @deprecated Use 'dictionary-lite' instead. +function flib_dictionary.check_skipped() + local script_data = global.__flib.dictionary + local tick = game.tick + for _, player_data in pairs(script_data.players) do + -- If it's been longer than the timeout, request the string again + -- This is to solve a very rare edge case where translations requested on the same tick that a singleplayer game + -- is saved will not be returned when that save is loaded + if (player_data.requested_tick or 0) + translation_timeout <= tick then + if player_data.status == "get_language" then + player_data.player.request_translation({ "", "FLIB_LOCALE_IDENTIFIER", separator, { "locale-identifier" } }) + end + if player_data.status == "translating" then + request_translation(player_data) + end + end + end +end + +--- Escape match special characters +local function match_literal(s) + return string.gsub(s, "%-", "%%-") +end + +--- @param dict_lang string +local function clean_gui(dict_lang) + for _, player in pairs(game.players) do + local window = mod_gui.get_frame_flow(player).flib_translation_progress + if window then + local pane = window.pane + local mod_flow = pane[script.mod_name] + if mod_flow then + local lang_flow = mod_flow[dict_lang] + if lang_flow then + lang_flow.destroy() + end + if #mod_flow.children == 1 then + mod_flow.destroy() + end + end + if #pane.children == 0 then + window.destroy() + end + end + end +end + +local dictionary_match_string = key_value("^FLIB_DICTIONARY_MOD", match_literal(script.mod_name)) + .. key_value("FLIB_DICTIONARY_NAME", "(.-)") + .. key_value("FLIB_DICTIONARY_LANGUAGE", "(.-)") + .. key_value("FLIB_DICTIONARY_STRING_INDEX", "(%d-)") + .. "(.*)$" + +--- @deprecated Use 'dictionary-lite' instead. +function flib_dictionary.process_translation(event_data) + if not event_data.translated then + return + end + local script_data = global.__flib.dictionary + if string.find(event_data.result, "FLIB_DICTIONARY_NAME") then + local _, _, dict_name, dict_lang, string_index, translation = + string.find(event_data.result, dictionary_match_string) + + if dict_name and dict_lang and string_index and translation then + local language_data = script_data.in_process[dict_lang] + -- In some cases, this can fire before on_configuration_changed + if not language_data then + return + end + local dictionary = language_data.dictionaries[dict_name] + if not dictionary then + return + end + local dict_data = raw[dict_name] + local player_data = script_data.players[event_data.player_index] + + -- If this number does not match, this is a duplicate, so ignore it + if tonumber(string_index) == player_data.i then + -- Extract current string's translations + for str in string.gmatch(translation, "(.-)" .. separator) do + local _, _, key, value = string.find(str, "^(.-)" .. inner_separator .. "(.-)$") + if key then + -- If `keep_untranslated` is true, then use the key as the value if it failed + local failed = string.find(value, "FLIB_TRANSLATION_FAILED") + if failed and dict_data.keep_untranslated then + value = key + elseif failed then + value = nil + end + if value then + dictionary[key] = value + end + language_data.translated_i = language_data.translated_i + 1 + end + end + + -- Request next translation + player_data.i = player_data.i + 1 + request_translation(player_data) + + -- GUI + for _, player in pairs(game.players) do + --- @type LuaGuiElement + local flow = mod_gui.get_frame_flow(player) + if not flow.flib_translation_progress then + gui.add(flow, { + type = "frame", + name = "flib_translation_progress", + style = mod_gui.frame_style, + style_mods = { width = 350 }, + direction = "vertical", + { + type = "label", + style = "frame_title", + caption = { "gui.flib-translating-dictionaries" }, + tooltip = { "gui.flib-translating-dictionaries-description" }, + }, + { + type = "frame", + name = "pane", + style = "inside_shallow_frame_with_padding", + style_mods = { top_padding = 8 }, + direction = "vertical", + }, + }) + end + local pane = flow.flib_translation_progress.pane --[[@as LuaGuiElement]] + if not pane[script.mod_name] then + gui.add(pane, { + type = "flow", + name = script.mod_name, + direction = "vertical", + { type = "label", style = "caption_label", style_mods = { top_margin = 4 }, caption = script.mod_name }, + }) + end + local mod_flow = pane[script.mod_name] --[[@as LuaGuiElement]] + if not mod_flow[dict_lang] then + gui.add(mod_flow, { + type = "flow", + name = dict_lang, + style_mods = { vertical_align = "center", horizontal_spacing = 8 }, + { type = "label", style = "bold_label", caption = dict_lang }, + { type = "progressbar", name = "bar", style_mods = { horizontally_stretchable = true } }, + { type = "label", name = "label", style = "bold_label" }, + }) + end + local progress = language_data.translated_i / raw._total_strings + mod_flow[dict_lang].bar.value = progress --[[@as double]] + mod_flow[dict_lang].label.caption = tostring(math.ceil(progress * 100)) .. "%" + mod_flow[dict_lang].label.tooltip = dict_name + .. "\n" + .. language_data.translated_i + .. " / " + .. raw._total_strings + end + + if player_data.status == "finished" then + -- Clean up translation data + script_data.translated[dict_lang] = language_data.dictionaries + script_data.in_process[dict_lang] = nil + for _, player_index in pairs(language_data.players) do + script_data.players[player_index] = nil + end + + -- Clean up GUI + clean_gui(dict_lang) + + return { dictionaries = language_data.dictionaries, language = dict_lang, players = language_data.players } + end + end + end + elseif string.find(event_data.result, "^FLIB_LOCALE_IDENTIFIER") then + local _, _, language = string.find(event_data.result, "^FLIB_LOCALE_IDENTIFIER" .. separator .. "(.*)$") + if language then + local player_data = script_data.players[event_data.player_index] + -- Handle duplicates + if not player_data or player_data.status ~= "get_language" then + return + end + + player_data.language = language + + -- Check if this language is already translated or being translated + local dictionaries = script_data.translated[language] + if dictionaries then + script_data.players[event_data.player_index] = nil + return { dictionaries = dictionaries, language = language, players = { event_data.player_index } } + end + local in_process = script_data.in_process[language] + if in_process then + table.insert(in_process.players, event_data.player_index) + player_data.status = "waiting" + return + end + + -- Set up player data for translating + player_data.status = "translating" + player_data.dictionary = next(raw, "_total_strings") + player_data.i = 1 + + -- Add language to in process data + script_data.in_process[language] = { + dictionaries = table.map(raw, function(_, k) + if k ~= "_total_strings" then + return {} + end + end), + players = { event_data.player_index }, + translated_i = 0, + } + + -- Start translating + request_translation(player_data) + end + end +end + +--- @deprecated Use 'dictionary-lite' instead. +function flib_dictionary.cancel_translation(player_index) + local script_data = global.__flib.dictionary + local player_data = script_data.players[player_index] + if not player_data then + return + end + -- Delete this player's data from global + script_data.players[player_index] = nil + + local in_process = script_data.in_process[player_data.language] + if not in_process then + return + end + + -- Remove this player from the players table + local i = table.find(in_process.players, player_index) + if i then + table.remove(in_process.players, i) + end + + if player_data.status ~= "translating" then + return + end + + -- Find the next player in the list with valid data + local next_player_data + for _, player_index in pairs(in_process.players) do + local player_data = script_data.players[player_index] + if player_data then + next_player_data = player_data + break + end + end + -- If there are no more valid players + if not next_player_data then + -- Completely cancel the translation + script_data.in_process[player_data.language] = nil + clean_gui(player_data.language) + return + end + --Update player info + next_player_data.status = "translating" + next_player_data.dictionary = player_data.dictionary + next_player_data.i = player_data.i + -- Resume translating with the new player + request_translation(next_player_data) +end + +--- @deprecated Use 'dictionary-lite' instead. +function flib_dictionary.set_use_local_storage(value) + use_local_storage = value +end + +return flib_dictionary diff --git a/.vscode/flib/direction.lua b/.vscode/flib/direction.lua new file mode 100644 index 0000000..50840e9 --- /dev/null +++ b/.vscode/flib/direction.lua @@ -0,0 +1,115 @@ +local flib_math = require("__flib__/math") + +--- Functions for working with directions. +--- @class flib_direction +local flib_direction = {} + +--- defines.direction.north +flib_direction.north = defines.direction.north +--- defines.direction.east +flib_direction.east = defines.direction.east +--- defines.direction.west +flib_direction.west = defines.direction.west +--- defines.direction.south +flib_direction.south = defines.direction.south +--- defines.direction.northeast +flib_direction.northeast = defines.direction.northeast +--- defines.direction.northwest +flib_direction.northwest = defines.direction.northwest +--- defines.direction.southeast +flib_direction.southeast = defines.direction.southeast +--- defines.direction.southwest +flib_direction.southwest = defines.direction.southwest + +--- Calculate the opposite direction. +--- @param direction defines.direction +--- @return defines.direction +function flib_direction.opposite(direction) + return (direction + 4) % 8 --[[@as defines.direction]] +end + +--- Calculate the next four-way or eight-way direction. +--- @param direction defines.direction +--- @param eight_way? boolean +--- @return defines.direction +function flib_direction.next(direction, eight_way) + return (direction + (eight_way and 1 or 2)) % 8 --[[@as defines.direction]] +end + +--- Calculate the previous four-way or eight-way direction. +--- @param direction defines.direction +--- @param eight_way? boolean +--- @return defines.direction +function flib_direction.previous(direction, eight_way) + return (direction + (eight_way and -1 or -2)) % 8 --[[@as defines.direction]] +end + +--- Calculate an orientation from a direction. +--- @param direction defines.direction +--- @return RealOrientation +function flib_direction.to_orientation(direction) + return direction / 8 --[[@as RealOrientation]] +end + +--- Calculate a vector from a direction. +--- @param direction defines.direction +--- @param distance? number default: `1` +--- @return MapPosition +function flib_direction.to_vector(direction, distance) + distance = distance or 1 + local x, y = 0, 0 + if direction == flib_direction.north then + y = y - distance + elseif direction == flib_direction.northeast then + x, y = x + distance, y - distance + elseif direction == flib_direction.east then + x = x + distance + elseif direction == flib_direction.southeast then + x, y = x + distance, y + distance + elseif direction == flib_direction.south then + y = y + distance + elseif direction == flib_direction.southwest then + x, y = x - distance, y + distance + elseif direction == flib_direction.west then + x = x - distance + elseif direction == flib_direction.northwest then + x, y = x - distance, y - distance + end + return { x = x, y = y } +end + +--- Calculate a two-dimensional vector from a cardinal direction. +--- @param direction defines.direction +--- @param longitudinal number Distance to move in the specified direction. +--- @param orthogonal number Distance to move perpendicular to the specified direction. A negative distance will move "left" and a positive distance will move "right" from the perspective of the direction. +--- @return MapPosition? +function flib_direction.to_vector_2d(direction, longitudinal, orthogonal) + if direction == defines.direction.north then + return { x = orthogonal, y = -longitudinal } + elseif direction == defines.direction.south then + return { x = -orthogonal, y = longitudinal } + elseif direction == defines.direction.east then + return { x = longitudinal, y = orthogonal } + elseif direction == defines.direction.west then + return { x = -longitudinal, y = -orthogonal } + end +end + +--- Calculate the direction of travel from the source to the target. +--- @param source MapPosition +--- @param target MapPosition +--- @param round? boolean If true, round to the nearest `defines.direction`. +--- @return defines.direction +function flib_direction.from_positions(source, target, round) + local deg = math.deg(math.atan2(target.y - source.y, target.x - source.x)) + local direction = (deg + 90) / 45 + if direction < 0 then + direction = direction + 8 + end + if round then + direction = flib_math.round(direction) + end + return direction --[[@as defines.direction]] +end + +return flib_direction diff --git a/.vscode/flib/docs/assets/indicator-examples.png b/.vscode/flib/docs/assets/indicator-examples.png new file mode 100644 index 0000000..86611a3 Binary files /dev/null and b/.vscode/flib/docs/assets/indicator-examples.png differ diff --git a/.vscode/flib/docs/assets/slot-style-examples.png b/.vscode/flib/docs/assets/slot-style-examples.png new file mode 100644 index 0000000..fe277c8 Binary files /dev/null and b/.vscode/flib/docs/assets/slot-style-examples.png differ diff --git a/.vscode/flib/docs/gui-styles.md b/.vscode/flib/docs/gui-styles.md new file mode 100644 index 0000000..919d33e --- /dev/null +++ b/.vscode/flib/docs/gui-styles.md @@ -0,0 +1,115 @@ +flib includes several GUI styles for your use and convenience. For help and information on how to use these styles effectively, refer to the [work-in-progress GUI style guide](https://github.com/raiguard/Factorio-SmallMods/wiki/GUI-Style-Guide). + +**IMPORTANT:** Modifying these styles in any way will modify them for all mods using them. Therefore, unless you are specifically creating a GUI skin mod, **DO NOT MODIFY THESE STYLES!** Instead, create your own new styles using these styles as parents, then modify those new styles as you wish. + +### Button styles + +**flib_selected_frame_action_button** + +A "selected" frame action button. Use when a frame action button can "toggle" on and off. + +**flib_selected_tool_button** + +A "selected" tool button. Use when a tool button can "toggle" on and off. + +**flib_tool_button_light_green** + +A light green tool button. Similar to the `item_and_count_select_confirm` style, but has margin and padding fixes to match other tool buttons. + +**flib_tool_button_dark_red** + +A dark red tool button, similar to the red shortcut button style. + +#### Slot styles + +flib includes a myriad of colored slot styles for use with `sprite-button`s: + +![](https://raw.githubusercontent.com/factoriolib/flib/master/docs/assets/slot-style-examples.png) + +There are three categories of style, from top to bottom: `slot`, `slot_button`, and `standalone_slot_button`. From left to right, the colors are `default`, `grey`, `red`, `orange`, `yellow`, `green`, `cyan`, `blue`, `purple`, and `pink`. + +The styles are formatted as `flib_CATEGORY_COLOR`. For example, if I want a pink standalone slot button (bottom-right on the preview image), I would use `flib_standalone_slot_button_pink`. + +Each slot style also has a `selected` variant, which uses the hovered graphics as default. This is intended to let a user "select" a button, and to let the mod visually distinguish it from other buttons around it. To use these styles, replace `flib_` with `flib_selected_` in the style you wish to use (e.g. `flib_selected_slot_button_green`). + +### Empty widget styles + +**flib_dialog_footer_drag_handle** + +A drag handle suitable for placement in the footer of a **dialog** window. + +**flib_dialog_footer_drag_handle_no_right** + +A dialog footer drag handle with the right margin removed. Suitable for dialog windows without a `confirm` button. + +**flib_dialog_titlebar_drag_handle** + +A drag handle suitable for placement in the titlebar of a **dialog** window. Use inside of a `flib_titlebar_flow` flow. + +**flib_horizontal_pusher** + +An invisible element that has `horizontally_stretchable` set, thereby "pushing" everything to the right. + +**flib_titlebar_drag_handle** + +A drag handle suitable for placement in the titlebar of a **standard** window (a window with a close button, or any other frame action buttons in the titlebar). Use inside of a `flib_titlebar_flow` flow. + +**flib_vertical_pusher** + +An invisible element that has `vertically_stretchable` set, thereby "pushing" everything to the bottom. + +### Flow styles + +**flib_indicator_flow** + +A flow designed for use with indicators (see below). + +**flib_titlebar_flow** + +A flow for use in a custom window titlebar. Identical to a regular horizontal flow, except for an increased horizontal spacing. + +### Frame styles + +**flib_shallow_frame_in_shallow_frame** + +A shallow frame nested in another shallow frame. Use of this is generally recommended against, but can be useful in some specific situations. + +### Image styles + +**flib_indicator** + +A 16x16 image style. Designed for use with flib's indicator sprites (see `sprites.md`). + +### Scroll pane styles + +**flib_naked_scroll_pane** + +A marginless scroll pane for use inside of content panes. When activated, it draws a shadow around its edges to give a more "inset" effect, to make it more obviously scrollable. The content is given an automatic 12px padding. + +**flib_naked_scroll_pane_under_tabs** + +Identical to `flib_naked_scroll_pane`, but has an inset on the top side when activated. Designed for use inside of a `tabbed_pane_with_no_side_padding` when not using a toolbar. + +**flib_naked_scroll_pane_no_padding** + +Identical to `flib_naked_scroll_pane`, but has no padding for the content that's put inside. Useful for wrapping a table in a scroll pane, for example. + +**flib_shallow_scroll_pane** + +A scroll pane that is inset from a shallow frame, instead of an outer frame. + +### Tabbed pane styles + +**flib_tabbed_pane_with_no_padding** + +A tabbed pane with no padding whatsoever on the content container. Useful for specific situations where you need to have full control of the content padding. + +### Textfield styles + +**flib_widthless_textfield** + +A textfield with no width defined on it. The default textfield style has a width of 200, which can wreak havoc. + +**flib_widthless_invalid_textfield** + +A widthless textfield that has a red background. Suitable for situations where the content of the textfield is invalid in some way. diff --git a/.vscode/flib/docs/sprites.md b/.vscode/flib/docs/sprites.md new file mode 100644 index 0000000..f5c3fa4 --- /dev/null +++ b/.vscode/flib/docs/sprites.md @@ -0,0 +1,9 @@ +The following are sprites provided by flib for your use: + +## Indicator sprites + +![](https://raw.githubusercontent.com/factoriolib/flib/master/docs/assets/indicator-examples.png) + +As seen above, indicator sprites are used to display the "status" of something. They are 16x16 in size, and are intended to be accompanied by a status label. The sprite names follow the `flib_indicator_COLOR` pattern, replacing `COLOR` with the corresponding color as shown in the preview above (e.g. `flib_indicator_blue`). + +To align the indicator with an adjacent label, create both the indicator and label as children of a flow with the `flib_indicator_flow` style (see `gui-styles.md`). \ No newline at end of file diff --git a/.vscode/flib/event.lua b/.vscode/flib/event.lua new file mode 100644 index 0000000..73d8ae6 --- /dev/null +++ b/.vscode/flib/event.lua @@ -0,0 +1,90 @@ +--- @diagnostic disable +--- @deprecated use `script` directly +local flib_event = {} + +for name, id in pairs(defines.events) do + flib_event[name] = function(handler, filters) + return script.on_event(id, handler, filters) + end +end + +--- @deprecated use `script` directly +function flib_event.on_init(handler) -- + script.on_init(handler) +end + +--- @deprecated use `script` directly +function flib_event.on_load(handler) -- + script.on_load(handler) +end + +--- @deprecated use `script` directly +function flib_event.on_configuration_changed(handler) -- + script.on_configuration_changed(handler) +end + +--- @deprecated use `script` directly +function flib_event.on_nth_tick(nth_tick, handler) -- + if handler then + script.on_nth_tick(nth_tick, handler) + else + script.on_nth_tick(nth_tick) + end +end + +--- @deprecated use `script` directly +function flib_event.register(ids, handler, filters) -- + if type(ids) ~= "table" then + ids = { ids } + end + for i = 1, #ids do + -- the game doesn't like you passing filters to events that don't support them, even if they're `nil` + if filters then + script.on_event(ids[i], handler, filters) + else + script.on_event(ids[i], handler) + end + end +end + +--- @deprecated use `script` directly +function flib_event.register_on_entity_destroyed(entity) -- + return script.register_on_entity_destroyed(entity) +end + +--- @deprecated use `script` directly +function flib_event.generate_id() -- + return script.generate_event_name() +end + +--- @deprecated use `script` directly +function flib_event.get_handler(id) -- + return script.get_event_handler(id) +end + +--- @deprecated use `script` directly +function flib_event.raise(id, event_data) -- + script.raise_event(id, event_data) +end + +--- @deprecated use `script` directly +function flib_event.get_order() -- + return script.get_event_order() +end + +--- @deprecated use `script` directly +function flib_event.set_filters(ids, filters) -- + if type(ids) ~= "table" then + ids = { ids } + end + for i = 1, #ids do + script.set_event_filter(ids[i], filters) + end +end + +--- @deprecated use `script` directly +function flib_event.get_filters(id) -- + script.get_event_filter(id) +end + +return flib_event diff --git a/.vscode/flib/format.lua b/.vscode/flib/format.lua new file mode 100644 index 0000000..66395b5 --- /dev/null +++ b/.vscode/flib/format.lua @@ -0,0 +1,79 @@ +--- Various string formatting functions. +--- @class flib_format +local flib_format = {} + +local suffix_list = { + { "Y", 1e24 }, -- yotta + { "Z", 1e21 }, -- zetta + { "E", 1e18 }, -- exa + { "P", 1e15 }, -- peta + { "T", 1e12 }, -- tera + { "G", 1e9 }, -- giga + { "M", 1e6 }, -- mega + { "k", 1e3 }, -- kilo +} + +--- Format a number for display, adding commas and an optional SI suffix. +--- Specify `fixed_precision` to display the number with the given width, +--- adjusting precision as necessary. +--- @param amount number +--- @param append_suffix boolean? +--- @param fixed_precision number? +--- @return string +function flib_format.number(amount, append_suffix, fixed_precision) + local suffix = "" + if append_suffix then + for _, data in ipairs(suffix_list) do + if math.abs(amount) >= data[2] then + amount = amount / data[2] + suffix = " " .. data[1] + break + end + end + if not fixed_precision then + amount = math.floor(amount * 10) / 10 + end + end + local formatted, k = tostring(amount), nil + if fixed_precision then + -- Show the number with fixed width precision + local len_before = #tostring(math.floor(amount)) + local len_after = math.max(0, fixed_precision - len_before - 1) + formatted = string.format("%." .. len_after .. "f", amount) + end + -- Add commas to result + while true do + formatted, k = string.gsub(formatted, "^(-?%d+)(%d%d%d)", "%1,%2") + if k == 0 then + break + end + end + return formatted .. suffix +end + +--- Convert the given tick or game.tick into "[hh:]mm:ss" format. +--- @param tick uint? +--- @param include_leading_zeroes boolean? +--- @return string +function flib_format.time(tick, include_leading_zeroes) + local total_seconds = math.floor((tick or game.ticks_played) / 60) + local seconds = total_seconds % 60 + local minutes = math.floor(total_seconds / 60) + if minutes > 59 then + minutes = minutes % 60 + local hours = math.floor(total_seconds / 3600) + if include_leading_zeroes then + return string.format("%02d:%02d:%02d", hours, minutes, seconds) + else + return string.format("%d:%02d:%02d", hours, minutes, seconds) + end + else + if include_leading_zeroes then + return string.format("%02d:%02d", minutes, seconds) + else + return string.format("%d:%02d", minutes, seconds) + end + end +end + +return flib_format diff --git a/.vscode/flib/graphics/black.png b/.vscode/flib/graphics/black.png new file mode 100644 index 0000000..3cf27f4 Binary files /dev/null and b/.vscode/flib/graphics/black.png differ diff --git a/.vscode/flib/graphics/dark-red-button.png b/.vscode/flib/graphics/dark-red-button.png new file mode 100644 index 0000000..661b7b1 Binary files /dev/null and b/.vscode/flib/graphics/dark-red-button.png differ diff --git a/.vscode/flib/graphics/empty.png b/.vscode/flib/graphics/empty.png new file mode 100644 index 0000000..a799dbb Binary files /dev/null and b/.vscode/flib/graphics/empty.png differ diff --git a/.vscode/flib/graphics/frame-action-icons.png b/.vscode/flib/graphics/frame-action-icons.png new file mode 100644 index 0000000..43e92ff Binary files /dev/null and b/.vscode/flib/graphics/frame-action-icons.png differ diff --git a/.vscode/flib/graphics/indicators.png b/.vscode/flib/graphics/indicators.png new file mode 100644 index 0000000..e43ce3d Binary files /dev/null and b/.vscode/flib/graphics/indicators.png differ diff --git a/.vscode/flib/graphics/planner.png b/.vscode/flib/graphics/planner.png new file mode 100644 index 0000000..c6003f5 Binary files /dev/null and b/.vscode/flib/graphics/planner.png differ diff --git a/.vscode/flib/graphics/slots.png b/.vscode/flib/graphics/slots.png new file mode 100644 index 0000000..098f51b Binary files /dev/null and b/.vscode/flib/graphics/slots.png differ diff --git a/.vscode/flib/graphics/slots.xcf b/.vscode/flib/graphics/slots.xcf new file mode 100644 index 0000000..1b8febd Binary files /dev/null and b/.vscode/flib/graphics/slots.xcf differ diff --git a/.vscode/flib/graphics/subheader-line.png b/.vscode/flib/graphics/subheader-line.png new file mode 100644 index 0000000..87ed0fd Binary files /dev/null and b/.vscode/flib/graphics/subheader-line.png differ diff --git a/.vscode/flib/gui-beta.lua b/.vscode/flib/gui-beta.lua new file mode 100644 index 0000000..b459c37 --- /dev/null +++ b/.vscode/flib/gui-beta.lua @@ -0,0 +1,2 @@ +--- @deprecated use `gui` or `gui-lite` instead.` +return require("__flib__/gui") diff --git a/.vscode/flib/gui-lite.lua b/.vscode/flib/gui-lite.lua new file mode 100644 index 0000000..1356680 --- /dev/null +++ b/.vscode/flib/gui-lite.lua @@ -0,0 +1,201 @@ +--- Utilities for building GUIs and handling GUI events. +--- @class flib_gui +local flib_gui = {} + +local handler_tag_key = "__" .. script.mod_name .. "_handler" + +--- @type table +local handlers = {} +--- @type table +local handlers_lookup = {} + +--- Add a new child or children to the given GUI element. +--- @param parent LuaGuiElement +--- @param def GuiElemDef Can be a single element, or an array of elements. +--- @param elems table? Optional initial `elems` table. +--- @return table elems Elements with names will be collected into this table. +--- @return LuaGuiElement first The element that was created first; the "top level" element. +function flib_gui.add(parent, def, elems) + if not parent or not parent.valid then + error("Parent element is missing or invalid") + end + if not elems then + elems = {} + end + -- If a single def was passed, wrap it in an array + if def.type or (def.tab and def.content) then + def = { def } + end + local first + for i = 1, #def do + local def = def[i] + if def.type then + -- Remove custom attributes from the def so the game doesn't serialize them + local children = def.children + local elem_mods = def.elem_mods + local handler = def.handler + local style_mods = def.style_mods + local drag_target = def.drag_target + -- If children were defined in the array portion, remove and collect them + local has_array_children = false + if def[1] then + if children then + error("Cannot define children in array portion and subtable simultaneously") + end + has_array_children = true + children = {} + for i = 1, #def do + children[i] = def[i] + def[i] = nil + end + end + def.children = nil + def.elem_mods = nil + def.handler = nil + def.style_mods = nil + def.drag_target = nil + + local elem = parent.add(def) + + if not first then + first = elem + end + if def.name then + elems[def.name] = elem + end + if style_mods then + for key, value in pairs(style_mods) do + elem.style[key] = value + end + end + if elem_mods then + for key, value in pairs(elem_mods) do + elem[key] = value + end + end + if drag_target then + local target = elems[drag_target] + if not target then + error("Drag target '" .. drag_target .. "' not found.") + end + elem.drag_target = target + end + if handler then + local out + if type(handler) == "table" then + out = {} + for name, handler in pairs(handler) do + out[tostring(name)] = handlers[handler] + end + else + out = handlers[handler] + end + local tags = elem.tags + tags[handler_tag_key] = out + elem.tags = tags + end + if children then + flib_gui.add(elem, children, elems) + end + + -- Re-add custom attributes + if children and has_array_children then + for i = 1, #children do + def[i] = children[i] + end + else + def.children = children + end + def.elem_mods = elem_mods + def.handler = handler + def.style_mods = style_mods + elseif def.tab and def.content then + local _, tab = flib_gui.add(parent, def.tab, elems) + local _, content = flib_gui.add(parent, def.content, elems) + parent.add_tab(tab, content) + end + end + return elems, first +end + +--- Add the given handler functions to the registry for use with `flib_gui.add`. Each handler must have a unique name. If a +--- `wrapper` function is provided, it will be called instead, and will receive the event data and handler. The wrapper +--- can be used to execute logic or gather data common to all handler functions for this GUI. +--- @param new_handlers table +--- @param wrapper fun(e: GuiEventData, handler: function)? +function flib_gui.add_handlers(new_handlers, wrapper) + for name, handler in pairs(new_handlers) do + if type(handler) == "function" then + if handlers_lookup[name] then + error("Attempted to register two GUI event handlers with the same name: " .. name) + end + handlers[handler] = name + if wrapper then + handlers_lookup[name] = function(e) + wrapper(e, handler) + end + else + handlers_lookup[name] = handler + end + end + end +end + +--- Dispatch the handler associated with this event and GUI element. The handler must have been added using +--- `flib_gui.add_handlers`. +--- @param e GuiEventData +--- @return boolean handled True if an event handler was called. +function flib_gui.dispatch(e) + local elem = e.element + if not elem then + return false + end + local tags = elem.tags --[[@as Tags]] + local handler_def = tags[handler_tag_key] + if not handler_def then + return false + end + local handler_type = type(handler_def) + if handler_type == "table" then + handler_def = handler_def[tostring(e.name)] + end + if handler_def then + local handler = handlers_lookup[handler_def] + if handler then + handler(e) + return true + end + end + return false +end + +--- Handle all GUI events with `flib_gui.dispatch`. Will not override any existing handlers. +function flib_gui.handle_events() + for name, id in pairs(defines.events) do + if string.find(name, "on_gui_") and not script.get_event_handler(id) then + script.on_event(id, flib_gui.dispatch) + end + end +end + +--- A GUI element definition. This extends `LuaGuiElement.add_param` with several new attributes. +--- Children may be defined in the array portion as an alternative to the `children` subtable. +--- @class GuiElemDefClass: LuaGuiElement.add_param +--- @field style_mods LuaStyle? Modifications to make to the element's style +--- @field elem_mods LuaGuiElement? Modifications to make to the element itself +--- @field drag_target string? Set the element's drag target to the element whose name matches this string. The drag target must be present in the `elems` table. +--- @field handler GuiElemHandler? Handler(s) to assign to this element +--- @field children GuiElemDef[]? Children to add to this element +--- @field tab GuiElemDef? To add a tab, specify `tab` and `content` and leave all other fields unset. +--- @field content GuiElemDef? To add a tab, specify `tab` and `content` and leave all other fields unset. + +--- @alias GuiElemDef GuiElemDefClass|GuiElemDef[] + +--- A handler function to invoke when receiving GUI events for this element. Alternatively, separate handlers may be +--- specified for different events. +--- @alias GuiElemHandler fun(e: GuiEventData)|table + +--- Aggregate type of all possible GUI events. +--- @alias GuiEventData EventData.on_gui_checked_state_changed|EventData.on_gui_click|EventData.on_gui_closed|EventData.on_gui_confirmed|EventData.on_gui_elem_changed|EventData.on_gui_location_changed|EventData.on_gui_opened|EventData.on_gui_selected_tab_changed|EventData.on_gui_selection_state_changed|EventData.on_gui_switch_state_changed|EventData.on_gui_text_changed|EventData.on_gui_value_changed + +return flib_gui diff --git a/.vscode/flib/gui.lua b/.vscode/flib/gui.lua new file mode 100644 index 0000000..1e10839 --- /dev/null +++ b/.vscode/flib/gui.lua @@ -0,0 +1,328 @@ +--- @diagnostic disable + +local mod_name = script.mod_name +local gui_event_defines = {} + +local event_id_to_string_mapping = {} +for name, id in pairs(defines.events) do + if string.find(name, "^on_gui") then + gui_event_defines[name] = id + event_id_to_string_mapping[id] = string.gsub(name, "^on_gui", "on") + end +end + +--- @deprecated use `gui-lite` instead +local flib_gui = {} + +--- @deprecated use `gui-lite` instead +function flib_gui.hook_events(callback) + local on_event = script.on_event + for _, id in pairs(gui_event_defines) do + on_event(id, callback) + end +end + +--- @deprecated use `gui-lite` instead +function flib_gui.read_action(event_data) + local elem = event_data.element + if not elem or not elem.valid then + return + end + + local mod_tags = elem.tags[mod_name] + if not mod_tags then + return + end + + local elem_actions = mod_tags.flib + if not elem_actions then + return + end + + local event_name = event_id_to_string_mapping[event_data.name] + local msg = elem_actions[event_name] + + return msg +end + +--- @deprecated use `gui-lite` instead +local function recursive_build(parent, structure, refs) + -- If the structure has no type, just ignore it + -- This is to make it possible to pass unit types `{}` to represent "no element" without breaking things + if not structure.type then + return + end + + -- Prepare tags + local original_tags = structure.tags + local tags = original_tags or {} + local actions = structure.actions + local tags_flib = tags.flib + tags.flib = actions + structure.tags = { [mod_name] = tags } + + -- Make the game not convert these into a property tree for no reason + structure.actions = nil + -- Substructures can be defined in special tables or as the array portion of this structure + local substructures + local substructures_len = #structure + if substructures_len > 0 then + if structure.children or structure.tabs then + error("Children or tab-and-content pairs must ALL be in the array portion, or a subtable. Not both at once!") + end + substructures = {} + for i = 1, substructures_len do + substructures[i] = structure[i] + structure[i] = nil + end + else + substructures = structure.children or structure.tabs + structure.children = nil + structure.tabs = nil + end + + -- Create element + local elem = parent.add(structure) + + -- Restore structure + structure.tags = original_tags + structure.actions = actions + tags.flib = tags_flib + + local style_mods = structure.style_mods + if style_mods then + for k, v in pairs(style_mods) do + elem.style[k] = v + end + end + + local elem_mods = structure.elem_mods + if elem_mods then + for k, v in pairs(elem_mods) do + elem[k] = v + end + end + + local ref = structure.ref + if ref then + -- Recursively create tables as needed + local prev = refs + local ref_length = #ref + for i = 1, ref_length - 1 do + local current_key = ref[i] + local current = prev[current_key] + if not current then + current = {} + prev[current_key] = current + end + prev = current + end + prev[ref[ref_length]] = elem + end + + -- Substructures + if substructures then + if structure.type == "tabbed-pane" then + local add_tab = elem.add_tab + for i = 1, #substructures do + local tab_and_content = substructures[i] + if not (tab_and_content.tab and tab_and_content.content) then + error("TabAndContent must have `tab` and `content` fields") + end + local tab = recursive_build(elem, tab_and_content.tab, refs) + local content = recursive_build(elem, tab_and_content.content, refs) + add_tab(tab, content) + end + else + for i = 1, #substructures do + recursive_build(elem, substructures[i], refs) + end + end + end + + return elem +end + +--- @deprecated use `gui-lite` instead +function flib_gui.build(parent, structures) + local refs = {} + for i = 1, #structures do + recursive_build(parent, structures[i], refs) + end + return refs +end + +--- @deprecated use `gui-lite` instead +function flib_gui.add(parent, structure) + -- Just in case they had a ref in the structure already, extract it + local previous_ref = structure.ref + -- Put in a known ref that we can use later + structure.ref = { "FLIB_ADD_ROOT" } + -- Build the element + local refs = {} + recursive_build(parent, structure, refs) + -- Restore the previous ref + structure.ref = previous_ref + -- Return the element + return refs.FLIB_ADD_ROOT +end + +--- @deprecated use `gui-lite` instead +local function recursive_update(elem, updates) + if updates.cb then + updates.cb(elem) + end + + if updates.style then + elem.style = updates.style + end + + if updates.style_mods then + for key, value in pairs(updates.style_mods) do + elem.style[key] = value + end + end + + if updates.elem_mods then + for key, value in pairs(updates.elem_mods) do + elem[key] = value + end + end + + if updates.tags then + flib_gui.update_tags(elem, updates.tags) + end + + -- TODO: This could be a lot better + if updates.actions then + for event_name, payload in pairs(updates.actions) do + flib_gui.set_action(elem, event_name, payload) + end + end + + local substructures + local substructures_len = #updates + if substructures_len > 0 then + if updates.children or updates.tabs then + error("Children or tab-and-content pairs must ALL be in the array portion, or a subtable. Not both at once!") + end + substructures = {} + for i = 1, substructures_len do + substructures[i] = updates[i] + updates[i] = nil + end + else + substructures = updates.children or updates.tabs + updates.children = nil + updates.tabs = nil + end + local subelements + if elem.type == "tabbed-pane" then + subelements = elem.tabs + else + subelements = elem.children + end + + if substructures then + for i, substructure in pairs(substructures) do + if substructure.tab or substructure.content then + local elem_tab_and_content = subelements[i] + if elem_tab_and_content then + local tab = elem_tab_and_content.tab + local tab_updates = substructures.tab + if tab and tab_updates then + recursive_update(tab, tab_updates) + end + local content = elem_tab_and_content.content + local content_updates = substructures.content + if content and content_updates then + recursive_update(content, content_updates) + end + end + elseif subelements[i] then + recursive_update(subelements[i], substructure) + end + end + end +end + +--- @deprecated use `gui-lite` instead +function flib_gui.update(elem, updates) + recursive_update(elem, updates) +end + +--- @deprecated use `gui-lite` instead +function flib_gui.get_tags(elem) + return elem.tags[mod_name] or {} +end + +--- @deprecated use `gui-lite` instead +function flib_gui.set_tags(elem, tags) + local elem_tags = elem.tags + elem_tags[mod_name] = tags + elem.tags = elem_tags +end + +--- @deprecated use `gui-lite` instead +function flib_gui.delete_tags(elem) + local elem_tags = elem.tags + elem_tags[mod_name] = nil + elem.tags = elem_tags +end + +--- @deprecated use `gui-lite` instead +function flib_gui.update_tags(elem, updates) + local elem_tags = elem.tags + local existing = elem_tags[mod_name] + + if not existing then + existing = {} + elem_tags[mod_name] = existing + end + + for k, v in pairs(updates) do + existing[k] = v + end + + elem.tags = elem_tags +end + +--- @deprecated use `gui-lite` instead +function flib_gui.set_action(elem, event_name, msg) + local elem_tags = elem.tags + local existing = elem_tags[mod_name] + + if not existing then + existing = {} + elem_tags[mod_name] = existing + end + + local actions = existing.flib + if not actions then + actions = {} + existing.flib = actions + end + + actions[event_name] = msg or nil + + elem.tags = elem_tags +end + +--- @deprecated use `gui-lite` instead +function flib_gui.get_action(elem, event_name) + local elem_tags = elem.tags + local existing = elem_tags[mod_name] + + if not existing then + return + end + + local actions = existing.flib + if not actions then + return + end + + return actions[event_name] +end + +return flib_gui diff --git a/.vscode/flib/info.json b/.vscode/flib/info.json new file mode 100644 index 0000000..d75c0a9 --- /dev/null +++ b/.vscode/flib/info.json @@ -0,0 +1,24 @@ +{ + "name": "flib", + "version": "0.12.4", + "title": "Factorio Library", + "author": "raiguard, Optera, justarandomgeek, Nexela", + "contact": "https://lists.sr.ht/~raiguard/factorio-mods-devel", + "homepage": "https://git.sr.ht/~raiguard/flib", + "description": "A set of high-quality, commonly-used utilities for creating Factorio mods.", + "factorio_version": "1.1", + "dependencies": [ "base >= 1.1.74" ], + "package": { + "ignore": [ + "*.code-workspace", + "crowdin.yml", + ".gitattributes", + ".gitignore", + ".kakrc", + ".mailmap", + "stylua.toml", + "tests", + ".vscode" + ] + } +} diff --git a/.vscode/flib/locale/af/dictionary.cfg b/.vscode/flib/locale/af/dictionary.cfg new file mode 100644 index 0000000..c641816 --- /dev/null +++ b/.vscode/flib/locale/af/dictionary.cfg @@ -0,0 +1,3 @@ +locale-identifier=af +locale-name=Afrikaans + diff --git a/.vscode/flib/locale/ar/dictionary.cfg b/.vscode/flib/locale/ar/dictionary.cfg new file mode 100644 index 0000000..52ae371 --- /dev/null +++ b/.vscode/flib/locale/ar/dictionary.cfg @@ -0,0 +1,3 @@ +locale-identifier=ar +locale-name=العَرَبِيَّة + diff --git a/.vscode/flib/locale/be/dictionary.cfg b/.vscode/flib/locale/be/dictionary.cfg new file mode 100644 index 0000000..1a34fcc --- /dev/null +++ b/.vscode/flib/locale/be/dictionary.cfg @@ -0,0 +1,2 @@ +locale-identifier=be +locale-name=Беларуская diff --git a/.vscode/flib/locale/bg/dictionary.cfg b/.vscode/flib/locale/bg/dictionary.cfg new file mode 100644 index 0000000..14edc52 --- /dev/null +++ b/.vscode/flib/locale/bg/dictionary.cfg @@ -0,0 +1,2 @@ +locale-identifier=bg +locale-name=български език diff --git a/.vscode/flib/locale/ca/dictionary.cfg b/.vscode/flib/locale/ca/dictionary.cfg new file mode 100644 index 0000000..e6c1cd7 --- /dev/null +++ b/.vscode/flib/locale/ca/dictionary.cfg @@ -0,0 +1,3 @@ +locale-identifier=ca +locale-name=Català + diff --git a/.vscode/flib/locale/cs/dictionary.cfg b/.vscode/flib/locale/cs/dictionary.cfg new file mode 100644 index 0000000..928a86b --- /dev/null +++ b/.vscode/flib/locale/cs/dictionary.cfg @@ -0,0 +1,3 @@ +locale-identifier=cs +locale-name=Čeština + diff --git a/.vscode/flib/locale/da/dictionary.cfg b/.vscode/flib/locale/da/dictionary.cfg new file mode 100644 index 0000000..744948b --- /dev/null +++ b/.vscode/flib/locale/da/dictionary.cfg @@ -0,0 +1,3 @@ +locale-identifier=da +locale-name=Dansk + diff --git a/.vscode/flib/locale/de/dictionary.cfg b/.vscode/flib/locale/de/dictionary.cfg new file mode 100644 index 0000000..e54173d --- /dev/null +++ b/.vscode/flib/locale/de/dictionary.cfg @@ -0,0 +1,3 @@ +locale-identifier=de +locale-name=Deutsch + diff --git a/.vscode/flib/locale/de/flib.cfg b/.vscode/flib/locale/de/flib.cfg new file mode 100644 index 0000000..45940eb --- /dev/null +++ b/.vscode/flib/locale/de/flib.cfg @@ -0,0 +1,10 @@ +[gui] +flib-keep-open=Offen bleiben +flib-settings=Einstellungen +flib-finishing=Abschließen... +flib-translating-dictionaries=Übersetze Wörterbücher [img=info] +flib-translating-dictionaries-description=Mods, die das Wörterbuchsystem der Factorio Library verwenden, fordern Übersetzungen im Laufe der Zeit an, um Suchen in Ihrer Muttersprache zu ermöglichen. Dieses Fenster zeigt den Fortschritt der Übersetzungen jeder Mod für jede gewünschte Sprache an. Dieses Fenster schließt sich automatisch sobald alle Übersetzungen abgeschlossen sind. + +[mod-name] +flib=Factorio Library + diff --git a/.vscode/flib/locale/el/dictionary.cfg b/.vscode/flib/locale/el/dictionary.cfg new file mode 100644 index 0000000..334f2d3 --- /dev/null +++ b/.vscode/flib/locale/el/dictionary.cfg @@ -0,0 +1,3 @@ +locale-identifier=el +locale-name=Ελληνικά + diff --git a/.vscode/flib/locale/en/dictionary.cfg b/.vscode/flib/locale/en/dictionary.cfg new file mode 100644 index 0000000..842ca05 --- /dev/null +++ b/.vscode/flib/locale/en/dictionary.cfg @@ -0,0 +1,2 @@ +locale-identifier=en +locale-name=English diff --git a/.vscode/flib/locale/en/flib.cfg b/.vscode/flib/locale/en/flib.cfg new file mode 100644 index 0000000..13b7027 --- /dev/null +++ b/.vscode/flib/locale/en/flib.cfg @@ -0,0 +1,11 @@ +[gui] +# For use with the frame action button sprites +flib-keep-open=Keep open +flib-settings=Settings +# Translation progress GUI +flib-finishing=Finishing... +flib-translating-dictionaries=Translating dictionaries [img=info] +flib-translating-dictionaries-description=Mods that use the Factorio Library's dictionary system request translations over time in order to allow text search in your native language. This GUI shows the progress of each mod's translations for each required language. This GUI will automatically remove itself once all translations have finished. + +[mod-name] +flib=Factorio Library diff --git a/.vscode/flib/locale/eo/dictionary.cfg b/.vscode/flib/locale/eo/dictionary.cfg new file mode 100644 index 0000000..d765a78 --- /dev/null +++ b/.vscode/flib/locale/eo/dictionary.cfg @@ -0,0 +1,2 @@ +locale-identifier=eo +locale-name=Esperanto diff --git a/.vscode/flib/locale/es-ES/dictionary.cfg b/.vscode/flib/locale/es-ES/dictionary.cfg new file mode 100644 index 0000000..185e9ac --- /dev/null +++ b/.vscode/flib/locale/es-ES/dictionary.cfg @@ -0,0 +1,3 @@ +locale-identifier=es-ES +locale-name=Español + diff --git a/.vscode/flib/locale/et/dictionary.cfg b/.vscode/flib/locale/et/dictionary.cfg new file mode 100644 index 0000000..b1872fb --- /dev/null +++ b/.vscode/flib/locale/et/dictionary.cfg @@ -0,0 +1,2 @@ +locale-identifier=et +locale-name=Eesti diff --git a/.vscode/flib/locale/fi/dictionary.cfg b/.vscode/flib/locale/fi/dictionary.cfg new file mode 100644 index 0000000..bc79788 --- /dev/null +++ b/.vscode/flib/locale/fi/dictionary.cfg @@ -0,0 +1,3 @@ +locale-identifier=fi +locale-name=Suomi + diff --git a/.vscode/flib/locale/fr/dictionary.cfg b/.vscode/flib/locale/fr/dictionary.cfg new file mode 100644 index 0000000..545ba34 --- /dev/null +++ b/.vscode/flib/locale/fr/dictionary.cfg @@ -0,0 +1,3 @@ +locale-identifier=fr +locale-name=Français + diff --git a/.vscode/flib/locale/fy-NL/dictionary.cfg b/.vscode/flib/locale/fy-NL/dictionary.cfg new file mode 100644 index 0000000..88937e6 --- /dev/null +++ b/.vscode/flib/locale/fy-NL/dictionary.cfg @@ -0,0 +1,2 @@ +locale-identifier=fy-NL +locale-name=Frisian diff --git a/.vscode/flib/locale/ga-IE/dictionary.cfg b/.vscode/flib/locale/ga-IE/dictionary.cfg new file mode 100644 index 0000000..16fccce --- /dev/null +++ b/.vscode/flib/locale/ga-IE/dictionary.cfg @@ -0,0 +1,2 @@ +locale-identifier=ga-IE +locale-name=Gaeilge diff --git a/.vscode/flib/locale/he/dictionary.cfg b/.vscode/flib/locale/he/dictionary.cfg new file mode 100644 index 0000000..cc058a6 --- /dev/null +++ b/.vscode/flib/locale/he/dictionary.cfg @@ -0,0 +1,3 @@ +locale-identifier=he +locale-name=עברית + diff --git a/.vscode/flib/locale/hr/dictionary.cfg b/.vscode/flib/locale/hr/dictionary.cfg new file mode 100644 index 0000000..a3749c2 --- /dev/null +++ b/.vscode/flib/locale/hr/dictionary.cfg @@ -0,0 +1,2 @@ +locale-identifier=hr +locale-name=Hrvatski diff --git a/.vscode/flib/locale/hu/dictionary.cfg b/.vscode/flib/locale/hu/dictionary.cfg new file mode 100644 index 0000000..f1a7357 --- /dev/null +++ b/.vscode/flib/locale/hu/dictionary.cfg @@ -0,0 +1,3 @@ +locale-identifier=hu +locale-name=Magyar + diff --git a/.vscode/flib/locale/id/dictionary.cfg b/.vscode/flib/locale/id/dictionary.cfg new file mode 100644 index 0000000..924ee0a --- /dev/null +++ b/.vscode/flib/locale/id/dictionary.cfg @@ -0,0 +1,2 @@ +locale-identifier=id +locale-name=Bahasa Indonesia diff --git a/.vscode/flib/locale/it/dictionary.cfg b/.vscode/flib/locale/it/dictionary.cfg new file mode 100644 index 0000000..8bc30b7 --- /dev/null +++ b/.vscode/flib/locale/it/dictionary.cfg @@ -0,0 +1,3 @@ +locale-identifier=it +locale-name=Italiano + diff --git a/.vscode/flib/locale/ja/dictionary.cfg b/.vscode/flib/locale/ja/dictionary.cfg new file mode 100644 index 0000000..7ddfdcb --- /dev/null +++ b/.vscode/flib/locale/ja/dictionary.cfg @@ -0,0 +1,3 @@ +locale-identifier=ja +locale-name=日本語 + diff --git a/.vscode/flib/locale/ko/dictionary.cfg b/.vscode/flib/locale/ko/dictionary.cfg new file mode 100644 index 0000000..440baed --- /dev/null +++ b/.vscode/flib/locale/ko/dictionary.cfg @@ -0,0 +1,3 @@ +locale-identifier=ko +locale-name=한국어 + diff --git a/.vscode/flib/locale/lt/dictionary.cfg b/.vscode/flib/locale/lt/dictionary.cfg new file mode 100644 index 0000000..9347b48 --- /dev/null +++ b/.vscode/flib/locale/lt/dictionary.cfg @@ -0,0 +1,2 @@ +locale-identifier=lt +locale-name=Lietuvių diff --git a/.vscode/flib/locale/lv/dictionary.cfg b/.vscode/flib/locale/lv/dictionary.cfg new file mode 100644 index 0000000..808f6a6 --- /dev/null +++ b/.vscode/flib/locale/lv/dictionary.cfg @@ -0,0 +1,2 @@ +locale-identifier=lv +locale-name=Latviešu diff --git a/.vscode/flib/locale/nl/dictionary.cfg b/.vscode/flib/locale/nl/dictionary.cfg new file mode 100644 index 0000000..f39ee4f --- /dev/null +++ b/.vscode/flib/locale/nl/dictionary.cfg @@ -0,0 +1,3 @@ +locale-identifier=nl +locale-name=Nederlands + diff --git a/.vscode/flib/locale/no/dictionary.cfg b/.vscode/flib/locale/no/dictionary.cfg new file mode 100644 index 0000000..f4c0aae --- /dev/null +++ b/.vscode/flib/locale/no/dictionary.cfg @@ -0,0 +1,3 @@ +locale-identifier=no +locale-name=Norsk + diff --git a/.vscode/flib/locale/pl/dictionary.cfg b/.vscode/flib/locale/pl/dictionary.cfg new file mode 100644 index 0000000..eded063 --- /dev/null +++ b/.vscode/flib/locale/pl/dictionary.cfg @@ -0,0 +1,3 @@ +locale-identifier=pl +locale-name=Polski + diff --git a/.vscode/flib/locale/pt-BR/dictionary.cfg b/.vscode/flib/locale/pt-BR/dictionary.cfg new file mode 100644 index 0000000..e66134f --- /dev/null +++ b/.vscode/flib/locale/pt-BR/dictionary.cfg @@ -0,0 +1,2 @@ +locale-identifier=pt-BR +locale-name=Português, Brasil diff --git a/.vscode/flib/locale/pt-PT/dictionary.cfg b/.vscode/flib/locale/pt-PT/dictionary.cfg new file mode 100644 index 0000000..6ec8f8a --- /dev/null +++ b/.vscode/flib/locale/pt-PT/dictionary.cfg @@ -0,0 +1,3 @@ +locale-identifier=pt-PT +locale-name=Português + diff --git a/.vscode/flib/locale/ro/dictionary.cfg b/.vscode/flib/locale/ro/dictionary.cfg new file mode 100644 index 0000000..d1c1684 --- /dev/null +++ b/.vscode/flib/locale/ro/dictionary.cfg @@ -0,0 +1,3 @@ +locale-identifier=ro +locale-name=Română + diff --git a/.vscode/flib/locale/ru/dictionary.cfg b/.vscode/flib/locale/ru/dictionary.cfg new file mode 100644 index 0000000..1cc706a --- /dev/null +++ b/.vscode/flib/locale/ru/dictionary.cfg @@ -0,0 +1,3 @@ +locale-identifier=ru +locale-name=Русский + diff --git a/.vscode/flib/locale/sk/dictionary.cfg b/.vscode/flib/locale/sk/dictionary.cfg new file mode 100644 index 0000000..cc8a985 --- /dev/null +++ b/.vscode/flib/locale/sk/dictionary.cfg @@ -0,0 +1,2 @@ +locale-identifier=sk +locale-name=Slovenčina diff --git a/.vscode/flib/locale/sl/dictionary.cfg b/.vscode/flib/locale/sl/dictionary.cfg new file mode 100644 index 0000000..f75aa6c --- /dev/null +++ b/.vscode/flib/locale/sl/dictionary.cfg @@ -0,0 +1,2 @@ +locale-identifier=sl +locale-name=Slovenščina diff --git a/.vscode/flib/locale/sq/dictionary.cfg b/.vscode/flib/locale/sq/dictionary.cfg new file mode 100644 index 0000000..ce232fd --- /dev/null +++ b/.vscode/flib/locale/sq/dictionary.cfg @@ -0,0 +1,2 @@ +locale-identifier=sq +locale-name=Shqip diff --git a/.vscode/flib/locale/sr/dictionary.cfg b/.vscode/flib/locale/sr/dictionary.cfg new file mode 100644 index 0000000..f6ac4f2 --- /dev/null +++ b/.vscode/flib/locale/sr/dictionary.cfg @@ -0,0 +1,3 @@ +locale-identifier=sr +locale-name=Српски + diff --git a/.vscode/flib/locale/sv-SE/dictionary.cfg b/.vscode/flib/locale/sv-SE/dictionary.cfg new file mode 100644 index 0000000..9b971b5 --- /dev/null +++ b/.vscode/flib/locale/sv-SE/dictionary.cfg @@ -0,0 +1,3 @@ +locale-identifier=sv-SE +locale-name=Svenska + diff --git a/.vscode/flib/locale/th/dictionary.cfg b/.vscode/flib/locale/th/dictionary.cfg new file mode 100644 index 0000000..3c7a558 --- /dev/null +++ b/.vscode/flib/locale/th/dictionary.cfg @@ -0,0 +1,2 @@ +locale-identifier=th +locale-name=ภาษาไทย diff --git a/.vscode/flib/locale/tr/dictionary.cfg b/.vscode/flib/locale/tr/dictionary.cfg new file mode 100644 index 0000000..1375eac --- /dev/null +++ b/.vscode/flib/locale/tr/dictionary.cfg @@ -0,0 +1,3 @@ +locale-identifier=tr +locale-name=Türkçe + diff --git a/.vscode/flib/locale/uk/dictionary.cfg b/.vscode/flib/locale/uk/dictionary.cfg new file mode 100644 index 0000000..36fd0ac --- /dev/null +++ b/.vscode/flib/locale/uk/dictionary.cfg @@ -0,0 +1,3 @@ +locale-identifier=uk +locale-name=Українська + diff --git a/.vscode/flib/locale/vi/dictionary.cfg b/.vscode/flib/locale/vi/dictionary.cfg new file mode 100644 index 0000000..9536727 --- /dev/null +++ b/.vscode/flib/locale/vi/dictionary.cfg @@ -0,0 +1,3 @@ +locale-identifier=vi +locale-name=Tiếng Việt Nam + diff --git a/.vscode/flib/locale/zh-CN/dictionary.cfg b/.vscode/flib/locale/zh-CN/dictionary.cfg new file mode 100644 index 0000000..fdd5c9f --- /dev/null +++ b/.vscode/flib/locale/zh-CN/dictionary.cfg @@ -0,0 +1,3 @@ +locale-identifier=zh-CN +locale-name=简体中文 + diff --git a/.vscode/flib/locale/zh-TW/dictionary.cfg b/.vscode/flib/locale/zh-TW/dictionary.cfg new file mode 100644 index 0000000..1fb4661 --- /dev/null +++ b/.vscode/flib/locale/zh-TW/dictionary.cfg @@ -0,0 +1,3 @@ +locale-identifier=zh-TW +locale-name=繁體中文 + diff --git a/.vscode/flib/math.lua b/.vscode/flib/math.lua new file mode 100644 index 0000000..fba4e5f --- /dev/null +++ b/.vscode/flib/math.lua @@ -0,0 +1,190 @@ +--- Extension of the Lua 5.2 math library. +--- @class flib_math: factorio.mathlib +local flib_math = {} + +local unpack = table.unpack + +-- Import lua math functions +for name, func in pairs(math) do + flib_math[name] = func +end + +--- Multiply by degrees to convert to radians. +--- ```lua +--- local rad = 1 x flib_math.deg_to_rad -- 0.0174533 +--- ``` +flib_math.deg_to_rad = flib_math.pi / 180 --- @type number + +--- Multiply by radians to convert to degrees. +--- +--- ```lua +--- local deg = 1 x flib_math.rad_to_deg -- 57.2958 +--- ``` +flib_math.rad_to_deg = 180 / flib_math.pi --- @type number + +flib_math.max_double = 0X1.FFFFFFFFFFFFFP+1023 +flib_math.min_double = -0X1.FFFFFFFFFFFFFP+1023 +flib_math.max_int8 = 127 --- 127 +flib_math.min_int8 = -128 --- -128 +flib_math.max_uint8 = 255 --- 255 +flib_math.max_int16 = 32767 --- 32,767 +flib_math.min_int16 = -32768 --- -32,768 +flib_math.max_uint16 = 65535 --- 65,535 +flib_math.max_int = 2147483647 --- 2,147,483,647 +flib_math.min_int = -2147483648 --- -2,147,483,648 +flib_math.max_uint = 4294967295 --- 4,294,967,295 +flib_math.max_int53 = 0x1FFFFFFFFFFFFF --- 9,007,199,254,740,991 +flib_math.min_int53 = -0x20000000000000 --- -9,007,199,254,740,992 + +--- Round a number to the nearest multiple of divisor. +--- Defaults to nearest integer if divisor is not provided. +--- +--- From [lua-users.org](http://lua-users.org/wiki/SimpleRound). +--- @param num number +--- @param divisor? number `num` will be rounded to the nearest multiple of `divisor` (default: 1). +--- @return number +function flib_math.round(num, divisor) + divisor = divisor or 1 + if num >= 0 then + return flib_math.floor((num / divisor) + 0.5) * divisor + else + return flib_math.ceil((num / divisor) - 0.5) * divisor + end +end + +--- Ceil a number to the nearest multiple of divisor. +--- @param num number +--- @param divisor? number `num` will be ceiled to the nearest multiple of `divisor` (default: 1). +function flib_math.ceiled(num, divisor) + if divisor then + return flib_math.ceil(num / divisor) * divisor + end + return flib_math.ceil(num) +end + +--- Floor a number to the nearest multiple of divisor. +--- @param num number +--- @param divisor? number `num` will be floored to the nearest multiple of `divisor` (default: 1). +function flib_math.floored(num, divisor) + if divisor then + return flib_math.floor(num / divisor) * divisor + end + return flib_math.floor(num) +end + +--- Round a number to the nearest N decimal places. +--- +--- From [lua-users.org](http://lua-users.org/wiki/SimpleRound). +--- @param num number +--- @param num_decimals number +--- @return number +--- @deprecated Use flib_math.round +function flib_math.round_to(num, num_decimals) + local mult = 10 ^ num_decimals + return flib_math.floor(num * mult + 0.5) / mult +end + +--- Ceil a number to N decimal places. +--- Use `math.ceil` directly if no decimals are needed. +--- @param num number +--- @param num_decimals number +--- @return number +--- @deprecated Use flib_math.ceiled +function flib_math.ceil_to(num, num_decimals) + local mult = 10 ^ num_decimals + return flib_math.ceil(num * mult) / mult +end + +--- Floor a number to N decimal places. +--- Use `math.floor` directly if no decimals are needed. +--- @param num number +--- @param num_decimals number +--- @return number +--- @deprecated use flib_math.floored +function flib_math.floor_to(num, num_decimals) + local mult = 10 ^ num_decimals + return flib_math.floor(num * mult) / mult +end + +--- Returns the argument with the maximum value from a set. +--- @param set number[] +--- @return number +function flib_math.maximum(set) + return flib_math.max(unpack(set)) +end + +--- Returns the argument with the minimum value from a set. +--- @param set number[] +--- @return number +function flib_math.minimum(set) + return flib_math.min(unpack(set)) +end + +--- Calculate the sum of a set of numbers. +--- @param set number[] +--- @return number +function flib_math.sum(set) + local sum = set[2] or 0 + for i = 2, #set do + sum = sum + set[i] + end + return sum +end + +--- Calculate the mean (average) of a set of numbers. +--- @param set number[] +--- @return number +function flib_math.mean(set) + return flib_math.sum(set) / #set +end + +--- Calculate the mean of the largest and the smallest values in a set of numbers. +--- @param set number[] +--- @return number +function flib_math.midrange(set) + return 0.5 * (flib_math.minimum(set) + flib_math.maximum(set)) +end + +--- Calculate the range in a set of numbers. +--- @param set number[] +--- @return number +function flib_math.range(set) + return flib_math.maximum(set) - flib_math.minimum(set) +end + +--- Clamp a number between minimum and maximum values. +--- @param x number +--- @param min? number default 0 +--- @param max? number default 1 +--- @return number +function flib_math.clamp(x, min, max) + x = x == 0 and 0 or x -- Treat -0 as 0 + min, max = min or 0, max or 1 + return x < min and min or (x > max and max or x) +end + +--- Return the signedness of a number as a multiplier. +--- @param x number +--- @return number +function flib_math.sign(x) + return (x >= 0 and 1) or -1 +end + +--- Linearly interpolate between `num1` and `num2` by `amount`. +--- +--- The parameter `amount` is clamped between `0` and `1`. +--- +--- When `amount = 0`, returns `num1`. +--- +--- When `amount = 1`, returns `num2`. +--- +--- When `amount = 0.5`, returns the midpoint of `num1` and `num2`. +--- @param num1 number +--- @param num2 number +--- @param amount number +--- @return number +function flib_math.lerp(num1, num2, amount) + return num1 + (num2 - num1) * flib_math.clamp(amount, 0, 1) +end + +return flib_math diff --git a/.vscode/flib/migration.lua b/.vscode/flib/migration.lua new file mode 100644 index 0000000..93a0e58 --- /dev/null +++ b/.vscode/flib/migration.lua @@ -0,0 +1,134 @@ +--- Mod migration and version comparison functions. +--- @class flib_migration +local flib_migration = {} + +local string = string +local table = table + +local version_pattern = "%d+" +local version_format = "%02d" + +--- Normalize version strings for easy comparison. +--- +--- # Examples +--- +--- ```lua +--- migration.format_version("1.10.1234", "%04d") +--- migration.format_version("3", "%02d") +--- ``` +--- @param version string +--- @param format string? default: `%02d` +--- @return string? +function flib_migration.format_version(version, format) + if version then + format = format or version_format + local tbl = {} + for v in string.gmatch(version, version_pattern) do + tbl[#tbl + 1] = string.format(format, v) + end + if next(tbl) then + return table.concat(tbl, ".") + end + end + return nil +end + +--- Check if current_version is newer than old_version. +--- @param old_version string +--- @param current_version string +--- @param format string? default: `%02d` +--- @return boolean? +function flib_migration.is_newer_version(old_version, current_version, format) + local v1 = flib_migration.format_version(old_version, format) + local v2 = flib_migration.format_version(current_version, format) + if v1 and v2 then + if v2 > v1 then + return true + end + return false + end + return nil +end + +--- Run migrations against the given version. +--- @param old_version string +--- @param migrations MigrationsTable +--- @param format? string default: `%02d` +--- @param ... any All additional arguments will be passed to each function within `migrations`. +function flib_migration.run(old_version, migrations, format, ...) + local migrate = false + for version, func in pairs(migrations) do + if migrate or flib_migration.is_newer_version(old_version, version, format) then + migrate = true + func(...) + end + end +end + +--- Determine if migrations need to be run for this mod, then run them if needed. +--- +--- # Examples +--- +--- ```lua +--- script.on_configuration_changed(function(e) +--- if migration.on_config_changed(e, migrations) then +--- -- Run generic (non-init) migrations +--- rebuild_prototype_data() +--- end +--- end +--- ``` +--- @param e ConfigurationChangedData +--- @param migrations? MigrationsTable +--- @param mod_name? string The mod to check against. Defaults to the current mod. +--- @param ... any All additional arguments will be passed to each function within `migrations`. +--- @return boolean run_generic_micrations +function flib_migration.on_config_changed(e, migrations, mod_name, ...) + local changes = e.mod_changes[mod_name or script.mod_name] + local old_version = changes and changes.old_version + if old_version or not changes then + if migrations then + flib_migration.run(old_version, migrations, nil, ...) + end + return true + end + return false +end + +--- Handle on_configuration_changed with the given generic and version-specific migrations. Will override any existing +--- on_configuration_changed event handler. Both arguments are optional. +--- @param version_migrations MigrationsTable? +--- @param generic_handler fun(e: ConfigurationChangedData)? +function flib_migration.handle_on_configuration_changed(version_migrations, generic_handler) + script.on_configuration_changed(function(e) + if flib_migration.on_config_changed(e, version_migrations) and generic_handler then + generic_handler(e) + end + end) +end + +return flib_migration + +--- Migration code to run for specific mod version. A given function will run if the previous mod version is less +--- than the given version. +--- +--- # Example +--- +--- ```lua +--- { +--- ["1.0.1"] = function() +--- global.foo = nil +--- for _, player_table in pairs(global.players) do +--- player_table.bar = "Lorem ipsum" +--- end +--- end, +--- ["1.0.7"] = function() +--- global.bar = {} +--- end +--- ["1.1.0"] = function(arg) +--- global.foo = arg +--- end +--- } +--- ``` +--- +--- If the mod is upgraded from 1.0.4 to 1.1.0, then the migrations for 1.0.7 and 1.1.0 will be run. +--- @alias MigrationsTable table diff --git a/.vscode/flib/misc.lua b/.vscode/flib/misc.lua new file mode 100644 index 0000000..8957894 --- /dev/null +++ b/.vscode/flib/misc.lua @@ -0,0 +1,72 @@ +--- @diagnostic disable +--- @deprecated use `format` and `position` modules instead.` +local flib_misc = {} + +local math = math +local string = string + +--- @deprecated use `flib_position.distance` instead.` +function flib_misc.get_distance(pos1, pos2) + local x1 = pos1.x or pos1[1] + local y1 = pos1.y or pos1[2] + local x2 = pos2.x or pos2[1] + local y2 = pos2.y or pos2[2] + return math.sqrt((x1 - x2) ^ 2 + (y1 - y2) ^ 2) +end + +--- @deprecated use `flib_position.distance_squared` instead.` +function flib_misc.get_distance_squared(pos1, pos2) + local x1 = pos1.x or pos1[1] + local y1 = pos1.y or pos1[2] + local x2 = pos2.x or pos2[1] + local y2 = pos2.y or pos2[2] + return (x1 - x2) ^ 2 + (y1 - y2) ^ 2 +end + +--- @deprecated Use `flib_format.time`. +function flib_misc.ticks_to_timestring(tick, include_leading_zeroes) + local total_seconds = math.floor((tick or game.ticks_played) / 60) + local seconds = total_seconds % 60 + local minutes = math.floor(total_seconds / 60) + if minutes > 59 then + minutes = minutes % 60 + local hours = math.floor(total_seconds / 3600) + if include_leading_zeroes then + return string.format("%02d:%02d:%02d", hours, minutes, seconds) + else + return string.format("%d:%02d:%02d", hours, minutes, seconds) + end + else + if include_leading_zeroes then + return string.format("%02d:%02d", minutes, seconds) + else + return string.format("%d:%02d", minutes, seconds) + end + end +end + +--- @deprecated Use `flib_format.number`. +function flib_misc.delineate_number(number, delimiter) + delimiter = delimiter or "," + -- Handle decimals + local _, _, before, after = string.find(tostring(number), "^(%d*)(%.%d*)") + if before and after then + number = tonumber(before) --[[@as number]] + after = after + else + before = math.floor(number) + after = "" + end + local formatted = before + local k + while true do + formatted, k = string.gsub(formatted, "^(-?%d+)(%d%d%d)", "%1" .. delimiter .. "%2") + if k == 0 then + break + end + end + return formatted .. after +end + +return flib_misc +--- @diagnostic enable diff --git a/.vscode/flib/on-tick-n.lua b/.vscode/flib/on-tick-n.lua new file mode 100644 index 0000000..e78add2 --- /dev/null +++ b/.vscode/flib/on-tick-n.lua @@ -0,0 +1,91 @@ +--- Schedule tasks to be executed later. +--- @class flib_on_tick_n +local on_tick_n = {} + +--- Initialize the module's script data table. +--- +--- Must be called at the **beginning** of `on_init`. Can also be used to delete all current tasks. +function on_tick_n.init() + if not global.__flib then + global.__flib = {} + end + --- @type table + global.__flib.on_tick_n = {} +end + +--- Retrieve the tasks for the given tick, if any. +--- +--- Must be called **during** `on_tick`. +--- @param tick number +--- @return Tasks? +function on_tick_n.retrieve(tick) + -- Failsafe for rare cases where on_tick can fire before on_init + if not global.__flib or not global.__flib.on_tick_n then + return + end + local actions = global.__flib.on_tick_n[tick] + if actions then + global.__flib.on_tick_n[tick] = nil + return actions + end +end + +--- Add a task to execute on the given tick. +--- @param tick number +--- @param task any The data representing this task. This can be anything except for a `function`. +--- @return TaskIdent ident An identifier for the task. Save this if you might remove the task before execution. +function on_tick_n.add(tick, task) + local list = global.__flib.on_tick_n + local tick_list = list[tick] + if tick_list then + local index = #tick_list + 1 + tick_list[index] = task + return { index = index, tick = tick } + else + list[tick] = { task } + return { index = 1, tick = tick } + end +end + +--- Remove a scheduled task. +--- @param ident TaskIdent The identifier object for the task, as returned from `on-tick-n.add`. +function on_tick_n.remove(ident) + local tick_list = global.__flib.on_tick_n[ident.tick] + if not tick_list or not tick_list[ident.index] then + return false + end + + tick_list[ident.index] = nil + + return true +end + +--- A unique identifier for a previously added task, used in `on-tick-n.remove`. +--- @class TaskIdent +--- @field tick number The tick this task is scheduled for. +--- @field index number The tasks' index in the tick's `Tasks` table. + +--- A table of tasks. +--- +--- Each task can be anything that is not a function, as specified in `on-tick-n.add`. +--- +--- **This is not an array, there may be gaps. Always use `pairs` to iterate this table.** +--- +--- # Example +--- +--- ```lua +--- event.on_tick(function(e) +--- for _, task in pairs(on_tick_n.retrieve(e.tick) or {}) do +--- if task == "say_hi" then +--- game.print("Hello there!") +--- elseif task == "order_66" then +--- for _, player in pairs(game.players) do +--- player.die() +--- end +--- end +--- end +--- end) +--- ``` +--- @alias Tasks table + +return on_tick_n diff --git a/.vscode/flib/orientation.lua b/.vscode/flib/orientation.lua new file mode 100644 index 0000000..12d9aa5 --- /dev/null +++ b/.vscode/flib/orientation.lua @@ -0,0 +1,39 @@ +--- Functions for working with orientations. +--- @class flib_orientation +local flib_orientation = {} + +flib_orientation.north = defines.direction.north / 8 +flib_orientation.east = defines.direction.east / 8 +flib_orientation.west = defines.direction.west / 8 +flib_orientation.south = defines.direction.south / 8 +flib_orientation.northeast = defines.direction.northeast / 8 +flib_orientation.northwest = defines.direction.northwest / 8 +flib_orientation.southeast = defines.direction.southeast / 8 +flib_orientation.southwest = defines.direction.southwest / 8 + +--- Returns a 4way or 8way direction from an orientation. +--- @param orientation number +--- @param eight_way boolean +--- @return defines.direction +function flib_orientation.to_direction(orientation, eight_way) + local ways = eight_way and 8 or 4 + local mod = eight_way and 1 or 2 + return math.floor(orientation * ways + 0.5) % ways * mod --[[@as defines.direction]] +end + +--- Returns the opposite orientation. +--- @param orientation number +--- @return number +function flib_orientation.opposite(orientation) + return (orientation + 0.5) % 1 +end + +--- Add two orientations together. +--- @param orientation1 number +--- @param orientation2 number +--- @return number the orientations added together +function flib_orientation.add(orientation1, orientation2) + return (orientation1 + orientation2) % 1 +end + +return flib_orientation diff --git a/.vscode/flib/position.lua b/.vscode/flib/position.lua new file mode 100644 index 0000000..715f8a0 --- /dev/null +++ b/.vscode/flib/position.lua @@ -0,0 +1,257 @@ +--- Utilities for manipulating positions. All functions support both the shorthand and explicit syntaxes and will +--- preserve the syntax that was passed in. +--- @class flib_position +local flib_position = {} + +--- FIXME: Sumneko doesn't properly handle generics yet and throws a bunch of bogus warnings. +--- @diagnostic disable + +--- Add two positions. +--- @generic P +--- @param pos1 `P` +--- @param pos2 `P` +--- @return P +function flib_position.add(pos1, pos2) + local x1 = pos1.x or pos1[1] + local y1 = pos1.y or pos1[2] + local x2 = pos2.x or pos2[1] + local y2 = pos2.y or pos2[2] + if pos1.x then + return { x = x1 + x2, y = y1 + y2 } + else + return { x1 + x2, y1 + y2 } + end +end + +--- Ceil the given position. +--- @generic P +--- @param pos `P` +--- @return P +function flib_position.ceil(pos) + if pos.x then + return { x = math.ceil(pos.x), y = math.ceil(pos.y) } + else + return { math.ceil(pos[1]), math.ceil(pos[2]) } + end +end + +--- Calculate the distance between two positions. +--- @generic P +--- @param pos1 `P` +--- @param pos2 `P` +--- @return number +function flib_position.distance(pos1, pos2) + local x1 = pos1.x or pos1[1] + local y1 = pos1.y or pos1[2] + local x2 = pos2.x or pos2[1] + local y2 = pos2.y or pos2[2] + return math.sqrt((x1 - x2) ^ 2 + (y1 - y2) ^ 2) +end + +--- Calculate the squared distance between two positions. +--- @generic P +--- @param pos1 `P` +--- @param pos2 `P` +--- @return number +function flib_position.distance_squared(pos1, pos2) + local x1 = pos1.x or pos1[1] + local y1 = pos1.y or pos1[2] + local x2 = pos2.x or pos2[1] + local y2 = pos2.y or pos2[2] + return (x1 - x2) ^ 2 + (y1 - y2) ^ 2 +end + +--- Divide two positions. +--- @generic P +--- @param pos1 `P` +--- @param pos2 `P` +--- @return P +function flib_position.div(pos1, pos2) + local x1 = pos1.x or pos1[1] + local y1 = pos1.y or pos1[2] + local x2 = pos2.x or pos2[1] + local y2 = pos2.y or pos2[2] + if pos1.x then + return { x = x1 / x2, y = y1 / y2 } + else + return { x1 / x2, y1 / y2 } + end +end + +--- Return the position in explicit form. +--- @generic P +--- @param pos `P` +--- @return P +function flib_position.ensure_explicit(pos) + if pos.x then + return pos + else + return { x = pos[1], y = pos[2] } + end +end + +--- Return the position in shorthand form. +--- @generic P +--- @param pos `P` +--- @return P +function flib_position.ensure_short(pos) + if pos.x then + return { pos.x, pos.y } + else + return pos + end +end + +--- Test if two positions are equal. +--- @generic P +--- @param pos1 `P` +--- @param pos2 `P` +--- @return boolean +function flib_position.eq(pos1, pos2) + local x1 = pos1.x or pos1[1] + local y1 = pos1.y or pos1[2] + local x2 = pos2.x or pos2[1] + local y2 = pos2.y or pos2[2] + return x1 == x2 and y1 == y2 +end + +--- Floor the given position. +--- @generic P +--- @param pos `P` +--- @return P +function flib_position.floor(pos) + if pos.x then + return { x = math.floor(pos.x), y = math.floor(pos.y) } + else + return { math.floor(pos[1]), math.floor(pos[2]) } + end +end + +--- Convert a `ChunkPosition` into a `TilePosition` by multiplying by 32. +--- @param pos ChunkPosition +--- @return TilePosition +function flib_position.from_chunk(pos) + if pos.x then + return { x = pos.x * 32, y = pos.y * 32 } + else + return { pos[1] * 32, pos[2] * 32 } + end +end + +--- Test if `pos1` is less than or equal to `pos2`. +--- @generic P +--- @param pos1 `P` +--- @param pos2 `P` +--- @return boolean +function flib_position.le(pos1, pos2) + local x1 = pos1.x or pos1[1] + local y1 = pos1.y or pos1[2] + local x2 = pos2.x or pos2[1] + local y2 = pos2.y or pos2[2] + return x1 <= x2 and y1 <= y2 +end + +--- Test if `pos1` is less than `pos2`. +--- @generic P +--- @param pos1 `P` +--- @param pos2 `P` +--- @return boolean +function flib_position.lt(pos1, pos2) + local x1 = pos1.x or pos1[1] + local y1 = pos1.y or pos1[2] + local x2 = pos2.x or pos2[1] + local y2 = pos2.y or pos2[2] + return x1 < x2 and y1 < y2 +end + +--- Take the remainder (modulus) of two positions. +--- @generic P +--- @param pos1 `P` +--- @param pos2 `P` +--- @return P +function flib_position.mod(pos1, pos2) + local x1 = pos1.x or pos1[1] + local y1 = pos1.y or pos1[2] + local x2 = pos2.x or pos2[1] + local y2 = pos2.y or pos2[2] + if pos1.x then + return { x = x1 % x2, y = y1 % y2 } + else + return { x1 % x2, y1 % y2 } + end +end + +--- Multiply two positions. +--- @generic P +--- @param pos1 `P` +--- @param pos2 `P` +--- @return P +function flib_position.mul(pos1, pos2) + local x1 = pos1.x or pos1[1] + local y1 = pos1.y or pos1[2] + local x2 = pos2.x or pos2[1] + local y2 = pos2.y or pos2[2] + if pos1.x then + return { x = x1 * x2, y = y1 * y2 } + else + return { x1 * x2, y1 * y2 } + end +end + +--- Subtract two positions. +--- @generic P +--- @param pos1 `P` +--- @param pos2 `P` +--- @return P +function flib_position.sub(pos1, pos2) + local x1 = pos1.x or pos1[1] + local y1 = pos1.y or pos1[2] + local x2 = pos2.x or pos2[1] + local y2 = pos2.y or pos2[2] + if pos1.x then + return { x = x1 - x2, y = y1 - y2 } + else + return { x1 - x2, y1 - y2 } + end +end + +--- Take the power of two positions. `pos1^pos2`. +--- @generic P +--- @param pos1 `P` +--- @param pos2 `P` +--- @return P +function flib_position.pow(pos1, pos2) + local x1 = pos1.x or pos1[1] + local y1 = pos1.y or pos1[2] + local x2 = pos2.x or pos2[1] + local y2 = pos2.y or pos2[2] + if pos1.x then + return { x = x1 ^ x2, y = y1 ^ y2 } + else + return { x1 ^ x2, y1 ^ y2 } + end +end + +--- Convert a `MapPosition` or `TilePosition` into a `ChunkPosition` by dividing by 32 and flooring. +--- @param pos MapPosition|TilePosition +--- @return ChunkPosition +function flib_position.to_chunk(pos) + if pos.x then + return { x = math.floor(pos.x / 32), y = math.floor(pos.y / 32) } + else + return { math.floor(pos[1] / 32), math.floor(pos[2] / 32) } + end +end + +--- Convert a `MapPosition` into a `TilePosition` by flooring. +--- @param pos MapPosition +--- @return TilePosition +function flib_position.to_tile(pos) + if pos.x then + return { x = math.floor(pos.x), y = math.floor(pos.y) } + else + return { math.floor(pos[1]), math.floor(pos[2]) } + end +end + +return flib_position diff --git a/.vscode/flib/prototypes/sprite.lua b/.vscode/flib/prototypes/sprite.lua new file mode 100644 index 0000000..0d91021 --- /dev/null +++ b/.vscode/flib/prototypes/sprite.lua @@ -0,0 +1,53 @@ +-- INDICATOR SPRITES + +local indicators = {} +for i, color in ipairs({ "black", "white", "red", "orange", "yellow", "green", "cyan", "blue", "purple", "pink" }) do + indicators[i] = { + type = "sprite", + name = "flib_indicator_" .. color, + filename = "__flib__/graphics/indicators.png", + y = (i - 1) * 32, + size = 32, + flags = { "icon" }, + } +end +data:extend(indicators) + +local fab = "__flib__/graphics/frame-action-icons.png" + +data:extend({ + { type = "sprite", name = "flib_pin_black", filename = fab, position = { 0, 0 }, size = 32, flags = { "icon" } }, + { type = "sprite", name = "flib_pin_white", filename = fab, position = { 32, 0 }, size = 32, flags = { "icon" } }, + { + type = "sprite", + name = "flib_pin_disabled", + filename = fab, + position = { 64, 0 }, + size = 32, + flags = { "icon" }, + }, + { + type = "sprite", + name = "flib_settings_black", + filename = fab, + position = { 0, 32 }, + size = 32, + flags = { "icon" }, + }, + { + type = "sprite", + name = "flib_settings_white", + filename = fab, + position = { 32, 32 }, + size = 32, + flags = { "icon" }, + }, + { + type = "sprite", + name = "flib_settings_disabled", + filename = fab, + position = { 64, 32 }, + size = 32, + flags = { "icon" }, + }, +}) diff --git a/.vscode/flib/prototypes/style.lua b/.vscode/flib/prototypes/style.lua new file mode 100644 index 0000000..dd389a8 --- /dev/null +++ b/.vscode/flib/prototypes/style.lua @@ -0,0 +1,335 @@ +local data_util = require("__flib__/data-util") + +local styles = data.raw["gui-style"].default + +-- SLOT BUTTON STYLES + +local slot_tileset = "__flib__/graphics/slots.png" + +local function gen_slot(x, y, default_offset) + default_offset = default_offset or 0 + return { + type = "button_style", + parent = "slot", + size = 40, + default_graphical_set = { + base = { border = 4, position = { x + default_offset, y }, size = 80, filename = slot_tileset }, + }, + hovered_graphical_set = { + base = { border = 4, position = { x + 80, y }, size = 80, filename = slot_tileset }, + }, + clicked_graphical_set = { + base = { border = 4, position = { x + 160, y }, size = 80, filename = slot_tileset }, + }, + disabled_graphical_set = { -- identical to default graphical set + base = { border = 4, position = { x + default_offset, y }, size = 80, filename = slot_tileset }, + }, + } +end + +local function gen_slot_button(x, y, default_offset, glow) + default_offset = default_offset or 0 + return { + type = "button_style", + parent = "slot_button", + size = 40, + default_graphical_set = { + base = { border = 4, position = { x + default_offset, y }, size = 80, filename = slot_tileset }, + shadow = _ENV.offset_by_2_rounded_corners_glow(_ENV.default_dirt_color), + }, + hovered_graphical_set = { + base = { border = 4, position = { x + 80, y }, size = 80, filename = slot_tileset }, + shadow = _ENV.offset_by_2_rounded_corners_glow(_ENV.default_dirt_color), + glow = _ENV.offset_by_2_rounded_corners_glow(glow), + }, + clicked_graphical_set = { + base = { border = 4, position = { x + 160, y }, size = 80, filename = slot_tileset }, + shadow = _ENV.offset_by_2_rounded_corners_glow(_ENV.default_dirt_color), + }, + disabled_graphical_set = { -- identical to default graphical set + base = { border = 4, position = { x + default_offset, y }, size = 80, filename = slot_tileset }, + shadow = _ENV.offset_by_2_rounded_corners_glow(_ENV.default_dirt_color), + }, + } +end + +local function gen_standalone_slot_button(x, y, default_offset) + default_offset = default_offset or 0 + return { + type = "button_style", + parent = "slot_button", + size = 40, + default_graphical_set = { + base = { border = 4, position = { x + default_offset, y }, size = 80, filename = slot_tileset }, + shadow = _ENV.offset_by_4_rounded_corners_subpanel_inset, + }, + hovered_graphical_set = { + base = { border = 4, position = { x + 80, y }, size = 80, filename = slot_tileset }, + shadow = _ENV.offset_by_4_rounded_corners_subpanel_inset, + }, + clicked_graphical_set = { + base = { border = 4, position = { x + 160, y }, size = 80, filename = slot_tileset }, + shadow = _ENV.offset_by_4_rounded_corners_subpanel_inset, + }, + disabled_graphical_set = { -- identical to default graphical set + base = { border = 4, position = { x + default_offset, y }, size = 80, filename = slot_tileset }, + shadow = _ENV.offset_by_4_rounded_corners_subpanel_inset, + }, + } +end + +local slot_data = { + { name = "default", y = 0, glow = _ENV.default_glow_color }, + { name = "grey", y = 80, glow = _ENV.default_glow_color }, + { name = "red", y = 160, glow = { 230, 135, 135 } }, + { name = "orange", y = 240, glow = { 216, 169, 122 } }, + { name = "yellow", y = 320, glow = { 230, 218, 135 } }, + { name = "green", y = 400, glow = { 153, 230, 135 } }, + { name = "cyan", y = 480, glow = { 135, 230, 230 } }, + { name = "blue", y = 560, glow = { 135, 186, 230 } }, + { name = "purple", y = 640, glow = { 188, 135, 230 } }, + { name = "pink", y = 720, glow = { 230, 135, 230 } }, +} + +for _, data in pairs(slot_data) do + styles["flib_slot_" .. data.name] = gen_slot(0, data.y) + styles["flib_selected_slot_" .. data.name] = gen_slot(0, data.y, 80) + styles["flib_slot_button_" .. data.name] = gen_slot_button(240, data.y, 0, data.glow) + styles["flib_selected_slot_button_" .. data.name] = gen_slot_button(240, data.y, 80, data.glow) + styles["flib_standalone_slot_button_" .. data.name] = gen_standalone_slot_button(240, data.y) + styles["flib_selected_standalone_slot_button_" .. data.name] = gen_standalone_slot_button(240, data.y, 80) +end + +-- BUTTON STYLES + +styles.flib_selected_frame_action_button = { + type = "button_style", + parent = "frame_action_button", + default_font_color = _ENV.button_hovered_font_color, + default_graphical_set = { + base = { position = { 225, 17 }, corner_size = 8 }, + shadow = { position = { 440, 24 }, corner_size = 8, draw_type = "outer" }, + }, + hovered_font_color = _ENV.button_hovered_font_color, + hovered_graphical_set = { + base = { position = { 369, 17 }, corner_size = 8 }, + shadow = { position = { 440, 24 }, corner_size = 8, draw_type = "outer" }, + }, + clicked_font_color = _ENV.button_hovered_font_color, + clicked_graphical_set = { + base = { position = { 352, 17 }, corner_size = 8 }, + shadow = { position = { 440, 24 }, corner_size = 8, draw_type = "outer" }, + }, + -- Simulate clicked-vertical-offset + top_padding = 1, + bottom_padding = -1, +} + +local btn = styles.button + +styles.flib_selected_tool_button = { + type = "button_style", + parent = "tool_button", + default_font_color = btn.selected_font_color, + default_graphical_set = btn.selected_graphical_set, + hovered_font_color = btn.selected_hovered_font_color, + hovered_graphical_set = btn.selected_hovered_graphical_set, + clicked_font_color = btn.selected_clicked_font_color, + clicked_graphical_set = btn.selected_clicked_graphical_set, + -- Simulate clicked-vertical-offset + top_padding = 1, + bottom_padding = -1, +} + +styles.flib_tool_button_light_green = { + type = "button_style", + parent = "item_and_count_select_confirm", + padding = 2, + top_margin = 0, +} + +styles.flib_tool_button_dark_red = { + type = "button_style", + parent = "tool_button", + default_graphical_set = { + base = { filename = data_util.dark_red_button_tileset, position = { 0, 0 }, corner_size = 8 }, + shadow = _ENV.default_dirt, + }, + hovered_graphical_set = { + base = { filename = data_util.dark_red_button_tileset, position = { 17, 0 }, corner_size = 8 }, + shadow = _ENV.default_dirt, + glow = _ENV.default_glow({ 236, 130, 130, 127 }, 0.5), + }, + clicked_graphical_set = { + base = { filename = data_util.dark_red_button_tileset, position = { 34, 0 }, corner_size = 8 }, + shadow = _ENV.default_dirt, + }, +} + +-- EMPTY-WIDGET STYLES + +styles.flib_dialog_footer_drag_handle = { + type = "empty_widget_style", + parent = "draggable_space", + height = 32, + horizontally_stretchable = "on", +} + +styles.flib_dialog_footer_drag_handle_no_right = { + type = "empty_widget_style", + parent = "flib_dialog_footer_drag_handle", + right_margin = 0, +} + +styles.flib_dialog_titlebar_drag_handle = { + type = "empty_widget_style", + parent = "flib_titlebar_drag_handle", + right_margin = 0, +} + +styles.flib_horizontal_pusher = { + type = "empty_widget_style", + horizontally_stretchable = "on", +} + +styles.flib_titlebar_drag_handle = { + type = "empty_widget_style", + parent = "draggable_space", + left_margin = 4, + right_margin = 4, + height = 24, + horizontally_stretchable = "on", +} + +styles.flib_vertical_pusher = { + type = "empty_widget_style", + vertically_stretchable = "on", +} + +-- FLOW STYLES + +styles.flib_indicator_flow = { + type = "horizontal_flow_style", + vertical_align = "center", +} + +styles.flib_titlebar_flow = { + type = "horizontal_flow_style", + horizontal_spacing = 8, +} + +-- FRAME STYLES + +styles.flib_shallow_frame_in_shallow_frame = { + type = "frame_style", + parent = "frame", + padding = 0, + graphical_set = { + base = { + position = { 85, 0 }, + corner_size = 8, + center = { position = { 76, 8 }, size = { 1, 1 } }, + draw_type = "outer", + }, + shadow = _ENV.default_inner_shadow, + }, + vertical_flow_style = { + type = "vertical_flow_style", + vertical_spacing = 0, + }, +} + +-- IMAGE STYLES + +styles.flib_indicator = { + type = "image_style", + size = 16, + stretch_image_to_widget_size = true, +} + +-- LINE STYLES + +styles.flib_subheader_horizontal_line = { + type = "line_style", + horizontally_stretchable = "on", + left_margin = -8, + right_margin = -8, + top_margin = -2, + bottom_margin = -2, + border = { + border_width = 8, + horizontal_line = { filename = "__flib__/graphics/subheader-line.png", size = { 1, 8 } }, + }, +} + +-- SCROLL-PANE STYLES + +styles.flib_naked_scroll_pane = { + type = "scroll_pane_style", + extra_padding_when_activated = 0, + padding = 12, + graphical_set = { + shadow = _ENV.default_inner_shadow, + }, +} + +styles.flib_naked_scroll_pane_under_tabs = { + type = "scroll_pane_style", + parent = "flib_naked_scroll_pane", + graphical_set = { + base = { + top = { position = { 93, 0 }, size = { 1, 8 } }, + draw_type = "outer", + }, + shadow = _ENV.default_inner_shadow, + }, +} + +styles.flib_naked_scroll_pane_no_padding = { + type = "scroll_pane_style", + parent = "flib_naked_scroll_pane", + padding = 0, +} + +styles.flib_shallow_scroll_pane = { + type = "scroll_pane_style", + padding = 0, + graphical_set = { + base = { position = { 85, 0 }, corner_size = 8, draw_type = "outer" }, + shadow = _ENV.default_inner_shadow, + }, +} + +-- TABBED PANE STYLES + +styles.flib_tabbed_pane_with_no_padding = { + type = "tabbed_pane_style", + tab_content_frame = { + type = "frame_style", + top_padding = 0, + bottom_padding = 0, + left_padding = 0, + right_padding = 0, + graphical_set = { + base = { + -- Same as tabbed_pane_graphical_set - but without bottom + top = { position = { 76, 0 }, size = { 1, 8 } }, + center = { position = { 76, 8 }, size = { 1, 1 } }, + }, + shadow = _ENV.top_shadow, + }, + }, +} + +-- TEXTFIELD STYLES + +styles.flib_widthless_textfield = { + type = "textbox_style", + width = 0, +} + +styles.flib_widthless_invalid_textfield = { + type = "textbox_style", + parent = "invalid_value_textfield", + width = 0, +} diff --git a/.vscode/flib/queue.lua b/.vscode/flib/queue.lua new file mode 100644 index 0000000..0a1a79b --- /dev/null +++ b/.vscode/flib/queue.lua @@ -0,0 +1,142 @@ +--- Lua queue implementation. +--- +--- Based on "Queues and Double Queues" from [Programming in Lua](http://www.lua.org/pil/11.4.html). +--- @class flib_queue +local flib_queue = {} + +---@class Queue: { [integer]: T, first: integer, last: integer } + +--- Create a new queue. +--- @return Queue +function flib_queue.new() + return { first = 0, last = -1 } +end + +--- Push an element into the front of the queue. +--- @generic T +--- @param self Queue +--- @param value T +function flib_queue.push_front(self, value) + local first = self.first - 1 + self.first = first + self[first] = value +end + +--- Push an element into the back of the queue. +--- @generic T +--- @param self Queue +--- @param value `T` +function flib_queue.push_back(self, value) + local last = self.last + 1 + self.last = last + self[last] = value +end + +--- Retrieve an element from the front of the queue. +--- @generic T +--- @param self Queue +--- @return T? +function flib_queue.pop_front(self) + local first = self.first + if first > self.last then + return + end + local value = self[first] + self[first] = nil + self.first = first + 1 + return value +end + +--- Retrieve an element from the back of the queue. +--- @generic T +--- @param self Queue +--- @return T? +function flib_queue.pop_back(self) + local last = self.last + if self.first > last then + return + end + local value = self[last] + self[last] = nil + self.last = last - 1 + return value +end + +--- Iterate over a queue's elements from the beginning to the end. +--- +--- # Example +--- +--- ```lua +--- local my_queue = queue.new() +--- for i = 1, 10 do +--- queue.push_back(my_queue, 1) +--- end +--- +--- -- 1 2 3 4 5 6 7 8 9 10 +--- for num in queue.iter(my_queue) do +--- log(i) +--- end +--- ``` +--- @generic T +--- @param self Queue +--- @return fun(self: Queue, index: integer): T +function flib_queue.iter(self) + local i = self.first - 1 + return function() + if i < self.last then + i = i + 1 + return i, self[i] + end + end +end + +--- Iterate over a queue's elements from the end to the beginning. +--- +--- # Example +--- +--- ```lua +--- local my_queue = queue.new() +--- for i = 1, 10 do +--- queue.push_back(my_queue, 1) +--- end +--- +--- -- 10 9 8 7 6 5 4 3 2 1 +--- for num in queue.iter_rev(my_queue) do +--- log(i) +--- end +--- ``` +--- @generic T +--- @param self Queue +--- @return fun(self: Queue, index: integer): T +function flib_queue.iter_rev(self) + local i = self.last + 1 + return function() + if i > self.first then + i = i - 1 + return i, self[i] + end + end +end + +--- Get the length of the queue. +--- @generic T +--- @param self Queue +--- @return number +function flib_queue.length(self) + return math.abs(self.last - self.first + 1) +end + +--- @deprecated Use `flib_queue.push_front` instead +flib_queue.push_left = flib_queue.push_front +--- @deprecated Use `flib_queue.push_back` instead +flib_queue.push_right = flib_queue.push_back +--- @deprecated Use `flib_queue.pop_front` instead +flib_queue.pop_left = flib_queue.pop_front +--- @deprecated Use `flib_queue.pop_back` instead +flib_queue.pop_right = flib_queue.pop_back +--- @deprecated Use `flib_queue.iter` instead +flib_queue.iter_left = flib_queue.iter +--- @deprecated Use `flib_queue.iter_rev` instead +flib_queue.iter_right = flib_queue.iter_rev + +return flib_queue diff --git a/.vscode/flib/reverse-defines.lua b/.vscode/flib/reverse-defines.lua new file mode 100644 index 0000000..68e4364 --- /dev/null +++ b/.vscode/flib/reverse-defines.lua @@ -0,0 +1,30 @@ +--- Defines reverse lookup table. +--- +--- NOTE: Type intellisense simply does not work for this module, and there is no easy way to fix +--- it. Use of this module is discouraged. +--- +--- # Example +--- +--- ```lua +--- event.on_built_entity(function(e) +--- local player = game.get_player(e.player_index) +--- local controller_name = reverse_defines.controllers[player.controller_type] +--- end) +--- ``` +local flib_reverse_defines = {} + +local function build_reverse_defines(lookup_table, base_table) + lookup_table = lookup_table or {} + for k, v in pairs(base_table) do + if type(v) == "table" then + lookup_table[k] = {} + build_reverse_defines(lookup_table[k], v) + else + lookup_table[v] = k + end + end +end + +build_reverse_defines(flib_reverse_defines, defines) + +return flib_reverse_defines diff --git a/.vscode/flib/table.lua b/.vscode/flib/table.lua new file mode 100644 index 0000000..bd3454e --- /dev/null +++ b/.vscode/flib/table.lua @@ -0,0 +1,552 @@ +--- Extension of the Lua 5.2 table library. +--- +--- **NOTE:** Several functions in this module will only work with [arrays](https://www.lua.org/pil/11.1.html), +--- which are tables with sequentially numbered keys. All table functions will work with arrays as well, but +--- array functions **will not** work with tables. +--- @class flib_table: tablelib +local flib_table = {} + +-- Import lua table functions +for name, func in pairs(table) do + flib_table[name] = func +end + +--- Shallow copy an array's values into a new array. +--- +--- This function is optimized specifically for arrays, and should be used in place of `table.shallow_copy` for arrays. +--- @param arr Array +--- @return Array +function flib_table.array_copy(arr) + local new_arr = {} + for i = 1, #arr do + new_arr[i] = arr[i] + end + return new_arr +end + +--- Merge all of the given arrays into a single array. +--- @param arrays Array An array of arrays to merge. +--- @return Array +function flib_table.array_merge(arrays) + local output = {} + local i = 0 + for j = 1, #arrays do + local arr = arrays[j] + for k = 1, #arr do + i = i + 1 + output[i] = arr[k] + end + end + return output +end + +--- Recursively compare two tables for inner equality. +--- +--- Does not compare metatables. +--- @param tbl1 table +--- @param tbl2 table +--- @return boolean +function flib_table.deep_compare(tbl1, tbl2) + if tbl1 == tbl2 then + return true + end + for k, v in pairs(tbl1) do + if type(v) == "table" and type(tbl2[k]) == "table" then + if not flib_table.deep_compare(v, tbl2[k]) then + return false + end + else + if v ~= tbl2[k] then + return false + end + end + end + for k in pairs(tbl2) do + if tbl1[k] == nil then + return false + end + end + return true +end + +--- Recursively copy the contents of a table into a new table. +--- +--- Does not create new copies of Factorio objects. +--- @generic T +--- @param tbl T The table to make a copy of. +--- @return T +function flib_table.deep_copy(tbl) + local lookup_table = {} + local function _copy(object) + if type(object) ~= "table" then + return object + -- don't copy factorio rich objects + elseif object.__self then + return object + elseif lookup_table[object] then + return lookup_table[object] + end + + local new_table = {} + lookup_table[object] = new_table + for index, value in pairs(object) do + new_table[_copy(index)] = _copy(value) + end + + return setmetatable(new_table, getmetatable(object)) + end + return _copy(tbl) +end + +--- Recursively merge two or more tables. +--- +--- Values from earlier tables are overwritten by values from later tables, unless both values are tables, in which case +--- they are recursively merged. +--- +--- Non-merged tables are deep-copied, so the result is brand-new. +--- +--- # Examples +--- +--- ```lua +--- local tbl = {foo = "bar"} +--- log(tbl.foo) -- logs "bar" +--- log (tbl.bar) -- errors (key is nil) +--- tbl = table.merge{tbl, {foo = "baz", set = 3}} +--- log(tbl.foo) -- logs "baz" +--- log(tbl.bar) -- logs "3" +--- ``` +--- @param tables Array An array of tables to merge. +--- @return table +function flib_table.deep_merge(tables) + local output = {} + for _, tbl in ipairs(tables) do + for k, v in pairs(tbl) do + if type(v) == "table" then + if type(output[k] or false) == "table" then + output[k] = flib_table.deep_merge({ output[k], v }) + else + output[k] = flib_table.deep_copy(v) + end + else + output[k] = v + end + end + end + return output +end + +--- Find and return the first key containing the given value. +--- +--- # Examples +--- +--- ```lua +--- local tbl = {"foo", "bar"} +--- local key_of_foo = table.find(tbl, "foo") -- 1 +--- local key_of_baz = table.find(tbl, "baz") -- nil +--- ``` +--- @generic K, V +--- @param tbl table The table to search. +--- @param value V The value to match. Must have an `eq` metamethod set, otherwise will error. +--- @return K? key The first key corresponding to `value`, if any. +function flib_table.find(tbl, value) + for k, v in pairs(tbl) do + if v == value then + return k + end + end +end + +--- Call the given function for each item in the table, and abort if the function returns truthy. +--- +--- Calls `callback(value, key)` for each item in the table, and immediately ceases iteration if the callback returns truthy. +--- +--- # Examples +--- +--- ```lua +--- local tbl = {1, 2, 3, 4, 5} +--- -- Run a function for each item (identical to a standard FOR loop) +--- table.for_each(tbl, function(v) game.print(v) end) +--- -- Determine if any value in the table passes the test +--- local value_is_even = table.for_each(tbl, function(v) return v % 2 == 0 end) +--- -- Determine if ALL values in the table pass the test (invert the test result and function return) +--- local all_values_less_than_six = not table.for_each(tbl, function(v) return not (v < 6) end) +--- ``` +--- @generic K, V +--- @param tbl table +--- @param callback fun(value: V, key: K): boolean Receives `value` and `key` as parameters. +--- @return boolean Whether the callback returned truthy for any one item, and thus halted iteration. +function flib_table.for_each(tbl, callback) + for k, v in pairs(tbl) do + if callback(v, k) then + return true + end + end + return false +end + +--- Call the given function on a set number of items in a table, returning the next starting key. +--- +--- Calls `callback(value, key)` over `n` items from `tbl`, starting after `from_k`. +--- +--- The first return value of each invocation of `callback` will be collected and returned in a table keyed by the +--- current item's key. +--- +--- The second return value of `callback` is a flag requesting deletion of the current item. +--- +--- The third return value of `callback` is a flag requesting that the iteration be immediately aborted. Use this flag to +--- early return on some condition in `callback`. When aborted, `for_n_of` will return the previous key as `from_k`, so +--- the next call to `for_n_of` will restart on the key that was aborted (unless it was also deleted). +--- +--- **DO NOT** delete entires from `tbl` from within `callback`, this will break the iteration. Use the deletion flag +--- instead. +--- +--- # Examples +--- +--- ```lua +--- local extremely_large_table = { +--- [1000] = 1, +--- [999] = 2, +--- [998] = 3, +--- ..., +--- [2] = 999, +--- [1] = 1000, +--- } +--- event.on_tick(function() +--- global.from_k = table.for_n_of(extremely_large_table, global.from_k, 10, function(v) game.print(v) end) +--- end) +--- ``` +--- @generic K, V, C +--- @param tbl table The table to iterate over. +--- @param from_k K The key to start iteration at, or `nil` to start at the beginning of `tbl`. If the key does not exist in `tbl`, it will be treated as `nil`, _unless_ a custom `_next` function is used. +--- @param n number The number of items to iterate. +--- @param callback fun(value: V, key: K):C,boolean,boolean Receives `value` and `key` as parameters. +--- @param _next? fun(tbl: table, from_k: K):K,V A custom `next()` function. If not provided, the default `next()` will be used. +--- @return K? next_key Where the iteration ended. Can be any valid table key, or `nil`. Pass this as `from_k` in the next call to `for_n_of` for `tbl`. +--- @return table results The results compiled from the first return of `callback`. +--- @return boolean reached_end Whether or not the end of the table was reached on this iteration. +function flib_table.for_n_of(tbl, from_k, n, callback, _next) + -- Bypass if a custom `next` function was provided + if not _next then + -- Verify start key exists, else start from scratch + if from_k and not tbl[from_k] then + from_k = nil + end + -- Use default `next` + _next = next + end + + local delete + local prev + local abort + local result = {} + + -- Run `n` times + for _ = 1, n, 1 do + local v + if not delete then + prev = from_k + end + from_k, v = _next(tbl, from_k) + if delete then + tbl[delete] = nil + end + + if from_k then + result[from_k], delete, abort = callback(v, from_k) + if delete then + delete = from_k + end + if abort then + break + end + else + return from_k, result, true + end + end + + if delete then + tbl[delete] = nil + from_k = prev + elseif abort then + from_k = prev + end + return from_k, result, false +end + +-- TODO: Remove array_insert, create separate array_filter function + +--- Create a filtered version of a table based on the results of a filter function. +--- +--- Calls `filter(value, key)` on each element in the table, returning a new table with only pairs for which +--- `filter` returned a truthy value. +--- +--- # Examples +--- +--- ```lua +--- local tbl = {1, 2, 3, 4, 5, 6} +--- local just_evens = table.filter(tbl, function(v) return v % 2 == 0 end) -- {[2] = 2, [4] = 4, [6] = 6} +--- local just_evens_arr = table.filter(tbl, function(v) return v % 2 == 0 end, true) -- {2, 4, 6} +--- ``` +--- @generic K, V +--- @param tbl table +--- @param filter fun(value: V, key: K): boolean +--- @param array_insert boolean? If true, the result will be constructed as an array of values that matched the filter. Key references will be lost. +--- @return table +function flib_table.filter(tbl, filter, array_insert) + local output = {} + local i = 0 + for k, v in pairs(tbl) do + if filter(v, k) then + if array_insert then + i = i + 1 + output[i] = v + else + output[k] = v + end + end + end + return output +end + +--- Retrieve the value at the key, or insert the default value. +--- @generic K, V +--- @param table table +--- @param key K +--- @param default_value V +--- @return V +function flib_table.get_or_insert(table, key, default_value) + local value = table[key] + if not value then + table[key] = default_value + return default_value + end + return value +end + +--- Invert the given table such that `[value] = key`, returning a new table. +--- +--- Non-unique values are overwritten based on the ordering from `pairs()`. +--- +--- # Examples +--- +--- ```lua +--- local tbl = {"foo", "bar", "baz", set = "baz"} +--- local inverted = table.invert(tbl) -- {foo = 1, bar = 2, baz = "set"} +--- ``` +--- @generic K, V +--- @param tbl table +--- @return table +function flib_table.invert(tbl) + local inverted = {} + for k, v in pairs(tbl) do + inverted[v] = k + end + return inverted +end + +--- Create a transformed table using the output of a mapper function. +--- +--- Calls `mapper(value, key)` on each element in the table, using the return as the new value for the key. +--- +--- # Examples +--- +--- ```lua +--- local tbl = {1, 2, 3, 4, 5} +--- local tbl_times_ten = table.map(tbl, function(v) return v * 10 end) -- {10, 20, 30, 40, 50} +--- ``` +--- @generic K, V, N +--- @param tbl table +--- @param mapper fun(value: V, key: V):N? +--- @return table +function flib_table.map(tbl, mapper) + local output = {} + for k, v in pairs(tbl) do + output[k] = mapper(v, k) + end + return output +end + +local function default_comp(a, b) + return a < b +end + +--- Partially sort an array. +--- +--- This function utilizes [insertion sort](https://en.wikipedia.org/wiki/Insertion_sort), which is _extremely_ inefficient with large data sets. However, you can spread the sorting over multiple ticks, reducing the performance impact. Only use this function if `table.sort` is too slow. +--- @generic V +--- @param arr Array +--- @param from_index number? The index to start iteration at (inclusive). Pass `nil` or a number less than `2` to begin at the start of the array. +--- @param iterations number The number of iterations to perform. Higher is more performance-heavy. This number should be adjusted based on the performance impact of the custom `comp` function (if any) and the size of the array. +--- @param comp fun(a: V, b: V) A comparison function for sorting. Must return truthy if `a < b`. +--- @return number? next_index The index to start the next iteration at, or `nil` if the end was reached. +function flib_table.partial_sort(arr, from_index, iterations, comp) + comp = comp or default_comp + local start_index = (from_index and from_index > 2) and from_index or 2 + local end_index = start_index + (iterations - 1) + + for j = start_index, end_index do + local key = arr[j] + if not key then + return nil + end + local i = j - 1 + + while i > 0 and comp(key, arr[i]) do + arr[i + 1] = arr[i] + i = i - 1 + end + + arr[i + 1] = key + end + + return end_index + 1 +end + +--- "Reduce" a table's values into a single output value, using the results of a reducer function. +--- +--- Calls `reducer(accumulator, value, key)` on each element in the table, returning a single accumulated output value. +--- +--- # Examples +--- +--- ```lua +--- local tbl = {10, 20, 30, 40, 50} +--- local sum = table.reduce(tbl, function(acc, v) return acc + v end) +--- local sum_minus_ten = table.reduce(tbl, function(acc, v) return acc + v end, -10) +--- ``` +--- @generic K, V, R +--- @param tbl table +--- @param reducer fun(acc: R, value: V, key: K):R +--- @param initial_value R? The initial value for the accumulator. If not provided or is falsy, the first value in the table will be used as the initial `accumulator` value and skipped as `key`. Calling `reduce()` on an empty table without an `initial_value` will cause a crash. +--- @return R +function flib_table.reduce(tbl, reducer, initial_value) + local accumulator = initial_value + for key, value in pairs(tbl) do + if accumulator then + accumulator = reducer(accumulator, value, key) + else + accumulator = value + end + end + return accumulator +end + +--- @deprecated use `table.remove`. +flib_table.retrieve = flib_table.remove + +--- Shallowly copy the contents of a table into a new table. +--- +--- The parent table will have a new table reference, but any subtables within it will still have the same table +--- reference. +--- +--- Does not copy metatables. +--- @generic T +--- @param tbl T +--- @param use_rawset boolean? Use rawset to set the values (ignores metamethods). +--- @return T The copied table. +function flib_table.shallow_copy(tbl, use_rawset) + local output = {} + for k, v in pairs(tbl) do + if use_rawset then + rawset(output, k, v) + else + output[k] = v + end + end + return output +end + +--- Shallowly merge two or more tables. +--- Unlike `table.deep_merge`, this will only combine the top level of the tables. +--- @param tables table[] +--- @return table +function flib_table.shallow_merge(tables) + local output = {} + for _, tbl in pairs(tables) do + for key, value in pairs(tbl) do + output[key] = value + end + end + return output +end + +--- Retrieve the size of a table. +--- +--- Uses Factorio's built-in `table_size` function. +--- @type fun(tbl: table):number +flib_table.size = _ENV.table_size + +--- Retrieve a shallow copy of a portion of an array, selected from `start` to `end` inclusive. +--- +--- The original array **will not** be modified. +--- +--- # Examples +--- +--- ```lua +--- local arr = {10, 20, 30, 40, 50, 60, 70, 80, 90} +--- local sliced = table.slice(arr, 3, 7) -- {30, 40, 50, 60, 70} +--- log(serpent.line(arr)) -- {10, 20, 30, 40, 50, 60, 70, 80, 90} (unchanged) +--- ``` +--- @generic V +--- @param arr Array +--- @param start number? default: `1` +--- @param stop number? Stop at this index. If zero or negative, will stop `n` items from the end of the array (default: `#arr`). +--- @return Array A new array with the copied values. +function flib_table.slice(arr, start, stop) + local output = {} + local n = #arr + + start = start or 1 + stop = stop or n + stop = stop <= 0 and (n + stop) or stop + + if start < 1 or start > n then + return {} + end + + local k = 1 + for i = start, stop do + output[k] = arr[i] + k = k + 1 + end + return output +end + +--- Extract a portion of an array, selected from `start` to `end` inclusive. +-- +--- The original array **will** be modified. +--- +--- # Examples +--- +--- ```lua +--- local arr = {10, 20, 30, 40, 50, 60, 70, 80, 90} +--- local spliced = table.splice(arr, 3, 7) -- {30, 40, 50, 60, 70} +--- log(serpent.line(arr)) -- {10, 20, 80, 90} (values were removed) +--- ``` +--- @generic V +--- @param arr Array +--- @param start number default: `1` +--- @param stop number? Stop at this index. If zero or negative, will stop `n` items from the end of the array (default: `#arr`). +--- @return Array A new array with the extracted values. +function flib_table.splice(arr, start, stop) + local output = {} + local n = #arr + + start = start or 1 + stop = stop or n + stop = stop <= 0 and (n + stop) or stop + + if start < 1 or start > n then + return {} + end + + local k = 1 + for _ = start, stop do + output[k] = table.remove(arr, start) + k = k + 1 + end + return output +end + +--- @class Array: { [integer]: T } + +return flib_table diff --git a/.vscode/flib/thumbnail.png b/.vscode/flib/thumbnail.png new file mode 100644 index 0000000..67f0865 Binary files /dev/null and b/.vscode/flib/thumbnail.png differ diff --git a/.vscode/flib/train.lua b/.vscode/flib/train.lua new file mode 100644 index 0000000..29587ee --- /dev/null +++ b/.vscode/flib/train.lua @@ -0,0 +1,130 @@ +--- Functions for working with trains. +--- @class flib_train +local flib_train = {} + +local table = table + +--- Get the main locomotive in a given train. +--- @param train LuaTrain +--- @return LuaEntity? locomotive The primary locomotive entity or `nil` when no locomotive was found +function flib_train.get_main_locomotive(train) + if + 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 + +--- Get the backer_name of the main locomotive in a given train. +--- @param train LuaTrain +--- @return string? backer_name The backer_name of the primary locomotive or `nil` when no locomotive was found +function flib_train.get_backer_name(train) + local loco = flib_train.get_main_locomotive(train) + return loco and loco.backer_name +end + +--- Rotate a single carriage of a train. +--- @param entity LuaEntity +--- @return boolean rotated `true` when rotation was successful. +function flib_train.rotate_carriage(entity) + local disconnected_back = entity.disconnect_rolling_stock(defines.rail_direction.back) + local disconnected_front = entity.disconnect_rolling_stock(defines.rail_direction.front) + entity.rotate() + -- Only reconnect the side that was disconnected + local reconnected_front = disconnected_front + local reconnected_back = disconnected_back + if disconnected_back then + reconnected_back = entity.connect_rolling_stock(defines.rail_direction.front) + end + if disconnected_front then + reconnected_front = entity.connect_rolling_stock(defines.rail_direction.back) + end + + if disconnected_front and not reconnected_front then + return false + end + if disconnected_back and not reconnected_back then + return false + end + return true +end + +--- Create a string representing train composition, and return a count of locomotives and wagons in the train. +--- - `L>`: Locomotives +--- - `C`: Cargo wagon +--- - `F`: Fluid wagon +--- - `A`: Artillery wagon +--- @param train LuaTrain +--- @return string? composition The composition string, or `nil` if the train was invalid. +--- @return TrainCompositionCounts? +function flib_train.get_composition_string(train) + if train and train.valid then + local carriages = train.carriages + local string_table = {} + local count_wagons, count_loco_front, count_loco_back, i = 0, 0, 0, 0 + local locos_front = train.locomotives.front_movers + for _, carriage in pairs(carriages) do + i = i + 1 + if carriage.type == "locomotive" then + local faces_forward = false + for _, loco in pairs(locos_front) do + if carriage.unit_number == loco.unit_number then + faces_forward = true + break + end + end + if faces_forward then + string_table[i] = " at line 310 jumps into the scope of local 'p_flag' require("scripts.constants") require("scripts.global") require("scripts.lib") diff --git a/cybersyn/data-final-fixes.lua b/cybersyn/data-final-fixes.lua index 8c16e75..176c80e 100644 --- a/cybersyn/data-final-fixes.lua +++ b/cybersyn/data-final-fixes.lua @@ -1,4 +1,5 @@ +--Credit to modo-lv for submitting the following code if mods["nullius"] then -- Place combinator in the same subgroup as the regular train stop data.raw["recipe"][COMBINATOR_NAME].subgroup = data.raw["train-stop"]["train-stop"].subgroup diff --git a/cybersyn/data.lua b/cybersyn/data.lua index 5d2a820..a16aa76 100644 --- a/cybersyn/data.lua +++ b/cybersyn/data.lua @@ -21,4 +21,10 @@ data:extend({ missing_train_icon, lost_train_icon, nonempty_train_icon, + { + type = "custom-input", + name = "cybersyn-toggle-planner", + key_sequence = "", + consuming = "game-only" + } }) diff --git a/cybersyn/graphics/combinator/cybernetic-combinator-remnants.png b/cybersyn/graphics/combinator/cybernetic-combinator-remnants.png index 583d417..1df52bb 100644 Binary files a/cybersyn/graphics/combinator/cybernetic-combinator-remnants.png and b/cybersyn/graphics/combinator/cybernetic-combinator-remnants.png differ diff --git a/cybersyn/graphics/combinator/cybernetic-combinator.png b/cybersyn/graphics/combinator/cybernetic-combinator.png index d95e376..83f5c16 100644 Binary files a/cybersyn/graphics/combinator/cybernetic-combinator.png and b/cybersyn/graphics/combinator/cybernetic-combinator.png differ diff --git a/cybersyn/graphics/combinator/hr-cybernetic-combinator-remnants.png b/cybersyn/graphics/combinator/hr-cybernetic-combinator-remnants.png index 5d7cdee..5cc4338 100644 Binary files a/cybersyn/graphics/combinator/hr-cybernetic-combinator-remnants.png and b/cybersyn/graphics/combinator/hr-cybernetic-combinator-remnants.png differ diff --git a/cybersyn/graphics/combinator/hr-cybernetic-combinator.png b/cybersyn/graphics/combinator/hr-cybernetic-combinator.png index bfdc9c4..a6da2dc 100644 Binary files a/cybersyn/graphics/combinator/hr-cybernetic-combinator.png and b/cybersyn/graphics/combinator/hr-cybernetic-combinator.png differ diff --git a/cybersyn/info.json b/cybersyn/info.json index 9baff2b..68d0c25 100644 --- a/cybersyn/info.json +++ b/cybersyn/info.json @@ -1,6 +1,6 @@ { "name": "cybersyn", - "version": "1.2.7", + "version": "1.2.8", "title": "Project Cybersyn", "author": "Mami", "factorio_version": "1.1", diff --git a/cybersyn/locale/en/base.cfg b/cybersyn/locale/en/base.cfg index 85e7ed2..8b0ea42 100644 --- a/cybersyn/locale/en/base.cfg +++ b/cybersyn/locale/en/base.cfg @@ -1,4 +1,5 @@ [mod-setting-name] +cybersyn-enable-planner=Enable central planning cybersyn-ticks-per-second=Central planning updates per second cybersyn-update-rate=Central planning update rate cybersyn-request-threshold=Default request threshold @@ -13,6 +14,7 @@ cybersyn-allow-cargo-in-depot=Allow cargo in depots cybersyn-invert-sign=Invert combinator output (deprecated) [mod-setting-description] +cybersyn-enable-planner=Enable or disable the central planning algorithm. If disabled no new trains will be dispatched. cybersyn-ticks-per-second=How many times per second the central planner should update the state of the network and schedule deliveries. This value will be rounded up to a divisor of 60. Setting this to 0 will stop all dispatches. cybersyn-update-rate=How many stations per tick can be polled at once or can have deliveries scheduled at once. Larger number allow the central planner to keep more up to date on the current state of the network, but at the cost of performance. cybersyn-request-threshold=The default request threshold when a request threshold signal is not given to a station. When a station receives a negative item signal that surpasses its request threshold, so long as any station exists with a positive signal greater than the request threshold, a delivery of that item will be scheduled between the two stations. @@ -26,6 +28,9 @@ cybersyn-stuck-train-time=After this many seconds from a train's dispatch, an al cybersyn-allow-cargo-in-depot=If checked, trains will be allowed to have cargo in depots; no alerts will be generated and the train will not be held. In addition, trains with orders to visit requester stations with "Inactivity condition" checked will wait for inactivity instead of waiting for empty cargo. Useful for creating train systems where depots handle excess cargo. For advanced users only. cybersyn-invert-sign=Flip the sign of the output of cybernetic combinators to be the same as it is in LTN or in earlier versions of Project Cybersyn. +[controls] +cybersyn-toggle-planner=Toggle central planner + [item-name] cybersyn-combinator=Cybernetic combinator @@ -88,3 +93,5 @@ use-same-depot-description=Require same depot use-same-depot-tooltip=When checked trains from this depot always return to this depot. When unchecked the train is allowed to return to any depot with the same name as this one. depot-bypass-description=Depot bypass depot-bypass-tooltip=When checked trains from this depot do not have to be parked at this depot to receive new orders, they just have to have no current active orders. They will still return to this depot if they are low on fuel and no refuelers are available. +enable-slot-barring-description=Bar unfiltered slots +enable-slot-barring-tooltip=When checked any slots of an adjacent wagon that are not filtered will instead be barred so no item can load into it. diff --git a/cybersyn/prototypes/tech.lua b/cybersyn/prototypes/tech.lua index b179b28..4e64122 100644 --- a/cybersyn/prototypes/tech.lua +++ b/cybersyn/prototypes/tech.lua @@ -33,6 +33,7 @@ if mods["pypostprocessing"] then cybersyn_tech.unit.ingredients[2] = nil end +--Credit to modo-lv for submitting the following code if mods["nullius"] then -- Enable recipe and place it just after regular station combinator_recipe.order = "nullius-eca" diff --git a/cybersyn/scripts/central-planning.lua b/cybersyn/scripts/central-planning.lua index 938ae76..33863ca 100644 --- a/cybersyn/scripts/central-planning.lua +++ b/cybersyn/scripts/central-planning.lua @@ -47,66 +47,66 @@ function create_delivery(map_data, r_station_id, p_station_id, train_id, manifes --NOTE: we assume that the train is not being teleported at this time --NOTE: set_manifest_schedule is allowed to cancel the delivery at the last second if applying the schedule to the train makes it lost if set_manifest_schedule(map_data, train.entity, depot.entity_stop, not train.use_any_depot, p_station.entity_stop, p_station.enable_inactive, r_station.entity_stop, mod_settings.allow_cargo_in_depot and r_station.enable_inactive--[[@as boolean]], manifest, is_at_depot) then - local old_status = train.status - train.status = STATUS_TO_P - train.p_station_id = p_station_id - train.r_station_id = r_station_id - train.manifest = manifest - train.last_manifest_tick = map_data.total_ticks + local old_status = train.status + train.status = STATUS_TO_P + train.p_station_id = p_station_id + train.r_station_id = r_station_id + train.manifest = manifest + train.last_manifest_tick = map_data.total_ticks - r_station.last_delivery_tick = map_data.total_ticks - p_station.last_delivery_tick = map_data.total_ticks + r_station.last_delivery_tick = map_data.total_ticks + p_station.last_delivery_tick = map_data.total_ticks - r_station.deliveries_total = r_station.deliveries_total + 1 - p_station.deliveries_total = p_station.deliveries_total + 1 + r_station.deliveries_total = r_station.deliveries_total + 1 + p_station.deliveries_total = p_station.deliveries_total + 1 - local r_is_each = r_station.network_name == NETWORK_EACH - local p_is_each = p_station.network_name == NETWORK_EACH - for item_i, item in ipairs(manifest) do - assert(item.count > 0, "main.lua error, transfer amount was not positive") + local r_is_each = r_station.network_name == NETWORK_EACH + local p_is_each = p_station.network_name == NETWORK_EACH + for item_i, item in ipairs(manifest) do + assert(item.count > 0, "main.lua error, transfer amount was not positive") - r_station.deliveries[item.name] = (r_station.deliveries[item.name] or 0) + item.count - p_station.deliveries[item.name] = (p_station.deliveries[item.name] or 0) - item.count + r_station.deliveries[item.name] = (r_station.deliveries[item.name] or 0) + item.count + p_station.deliveries[item.name] = (p_station.deliveries[item.name] or 0) - item.count - if item_i > 1 or r_is_each or p_is_each then - local f, a - if r_is_each then - f, a = pairs(r_station.network_flag--[[@as {[string]: int}]]) - if p_is_each then - for network_name, _ in f, a do - local item_network_name = network_name..":"..item.name - economy.all_r_stations[item_network_name] = nil - economy.all_p_stations[item_network_name] = nil - end - f, a = pairs(p_station.network_flag--[[@as {[string]: int}]]) + if item_i > 1 or r_is_each or p_is_each then + local f, a + if r_is_each then + f, a = pairs(r_station.network_flag--[[@as {[string]: int}]]) + if p_is_each then + for network_name, _ in f, a do + local item_network_name = network_name..":"..item.name + economy.all_r_stations[item_network_name] = nil + economy.all_p_stations[item_network_name] = nil end - elseif p_is_each then f, a = pairs(p_station.network_flag--[[@as {[string]: int}]]) - else - f, a = once, r_station.network_name - end - --prevent deliveries from being processed for these items until their stations are re-polled - --if we don't wait until they are repolled a duplicate delivery might be generated for stations that share inventories - for network_name, _ in f, a do - local item_network_name = network_name..":"..item.name - economy.all_r_stations[item_network_name] = nil - economy.all_p_stations[item_network_name] = nil end + elseif p_is_each then + f, a = pairs(p_station.network_flag--[[@as {[string]: int}]]) + else + f, a = once, r_station.network_name + end + --prevent deliveries from being processed for these items until their stations are re-polled + --if we don't wait until they are repolled a duplicate delivery might be generated for stations that share inventories + for network_name, _ in f, a do + local item_network_name = network_name..":"..item.name + economy.all_r_stations[item_network_name] = nil + economy.all_p_stations[item_network_name] = nil end end - - set_comb2(map_data, p_station) - set_comb2(map_data, r_station) - - p_station.display_state = 1 - update_display(map_data, p_station) - r_station.display_state = 1 - update_display(map_data, r_station) - - interface_raise_train_status_changed(train_id, old_status, STATUS_TO_P) - else - interface_raise_train_dispatch_failed(train_id) end + + set_comb2(map_data, p_station) + set_comb2(map_data, r_station) + + p_station.display_state = 1 + update_display(map_data, p_station) + r_station.display_state = 1 + update_display(map_data, r_station) + + interface_raise_train_status_changed(train_id, old_status, STATUS_TO_P) +else + interface_raise_train_dispatch_failed(train_id) +end end ---@param map_data MapData ---@param r_station_id uint @@ -654,9 +654,39 @@ local function tick_poll_station(map_data, mod_settings) end ---@param map_data MapData ---@param mod_settings CybersynModSettings -function tick_init(map_data, mod_settings) +function tick_poll_entities(map_data, mod_settings) local tick_data = map_data.tick_data + --NOTE: the following have undefined behaviour if the item on tick_data is deleted + if map_data.total_ticks%5 == 0 then + local train_id, train = next(map_data.trains, tick_data.last_train) + tick_data.last_train = train_id + if train then + if train.manifest and not train.se_is_being_teleported and train.last_manifest_tick + mod_settings.stuck_train_time*mod_settings.tps < map_data.total_ticks then + if mod_settings.stuck_train_alert_enabled then + send_alert_stuck_train(map_data, train.entity) + end + interface_raise_train_stuck(train_id) + end + end + + local refueler_id, _ = next(map_data.each_refuelers, tick_data.last_refueler) + tick_data.last_refueler = refueler_id + if refueler_id then + set_refueler_from_comb(map_data, mod_settings, refueler_id) + end + else + local comb_id, comb = next(map_data.to_comb, tick_data.last_comb) + tick_data.last_comb = comb_id + if comb and comb.valid then + combinator_update(map_data, comb, true) + end + end +end +---@param map_data MapData +---@param mod_settings CybersynModSettings +function tick_init(map_data, mod_settings) + map_data.economy.all_p_stations = {} map_data.economy.all_r_stations = {} map_data.economy.all_names = {} @@ -667,6 +697,7 @@ function tick_init(map_data, mod_settings) if station.last_delivery_tick + mod_settings.warmup_time*mod_settings.tps < map_data.total_ticks then map_data.active_station_ids[#map_data.active_station_ids + 1] = id map_data.warmup_station_ids[i] = nil + combinator_update(map_data, station.entity_comb1) end else map_data.warmup_station_ids[i] = nil @@ -686,30 +717,6 @@ function tick_init(map_data, mod_settings) map_data.queue_station_update = nil end - --NOTE: the following has undefined behavior if last_train is deleted, this should be ok since the following doesn't care how inconsistent our access pattern is - local train_id, train = next(map_data.trains, tick_data.last_train) - tick_data.last_train = train_id - - if train and train.manifest and not train.se_is_being_teleported and train.last_manifest_tick + mod_settings.stuck_train_time*mod_settings.tps < map_data.total_ticks then - if mod_settings.stuck_train_alert_enabled then - send_alert_stuck_train(map_data, train.entity) - end - interface_raise_train_stuck(train_id) - end - - --NOTE: the following has undefined behavior if last_comb is deleted - local comb_id, comb = next(map_data.to_comb, tick_data.last_comb) - tick_data.last_comb = comb_id - local refueler_id, _ = next(map_data.each_refuelers, tick_data.last_refueler) - tick_data.last_refueler = refueler_id - - if comb and comb.valid then - combinator_update(map_data, comb, true) - end - if refueler_id then - set_refueler_from_comb(map_data, mod_settings, refueler_id) - end - map_data.tick_state = STATE_POLL_STATIONS interface_raise_tick_init() end @@ -718,21 +725,30 @@ end function tick(map_data, mod_settings) map_data.total_ticks = map_data.total_ticks + 1 + if map_data.active_alerts then if map_data.total_ticks%(8*mod_settings.tps) < 1 then process_active_alerts(map_data) end end - if map_data.tick_state == STATE_INIT then - tick_init(map_data, mod_settings) - elseif map_data.tick_state == STATE_POLL_STATIONS then - for i = 1, mod_settings.update_rate do - if tick_poll_station(map_data, mod_settings) then break end + tick_poll_entities(map_data, mod_settings) + + if mod_settings.enable_planner then + if map_data.tick_state == STATE_INIT then + tick_init(map_data, mod_settings) end - elseif map_data.tick_state == STATE_DISPATCH then - for i = 1, mod_settings.update_rate do - tick_dispatch(map_data, mod_settings) + + if map_data.tick_state == STATE_POLL_STATIONS then + for i = 1, mod_settings.update_rate do + if tick_poll_station(map_data, mod_settings) then break end + end + elseif map_data.tick_state == STATE_DISPATCH then + for i = 1, mod_settings.update_rate do + tick_dispatch(map_data, mod_settings) + end end + else + map_data.tick_state = STATE_INIT end end diff --git a/cybersyn/scripts/constants.lua b/cybersyn/scripts/constants.lua index bef2e6f..999edcb 100644 --- a/cybersyn/scripts/constants.lua +++ b/cybersyn/scripts/constants.lua @@ -27,6 +27,7 @@ SETTING_IS_STACK = 3 SETTING_ENABLE_INACTIVE = 4 SETTING_USE_ANY_DEPOT = 5 SETTING_DISABLE_DEPOT_BYPASS = 6 +SETTING_ENABLE_SLOT_BARRING = 7 NETWORK_SIGNAL_DEFAULT = {name="signal-A", type="virtual"} NETWORK_EACH = "signal-each" diff --git a/cybersyn/scripts/factorio-api.lua b/cybersyn/scripts/factorio-api.lua index 8114553..2f5e785 100644 --- a/cybersyn/scripts/factorio-api.lua +++ b/cybersyn/scripts/factorio-api.lua @@ -1,5 +1,5 @@ --By Mami -local get_distance = require("__flib__.misc").get_distance +local get_distance = require("__flib__.position").distance local table_insert = table.insert local bit_extract = bit32.extract local bit_replace = bit32.replace @@ -21,7 +21,7 @@ end function get_stop_dist(entity0, entity1) local surface0 = entity0.surface.index local surface1 = entity1.surface.index - return (surface0 == surface1 and get_distance(entity0.position, entity1.position) or DIFFERENT_SURFACE_DISTANCE)--[[@as number]] + return (surface0 == surface1 and get_distance(entity0.position, entity1.position) or DIFFERENT_SURFACE_DISTANCE) end @@ -566,7 +566,7 @@ function get_comb_gui_settings(comb) elseif op == MODE_WAGON then selected_index = 5 end - return selected_index, params.first_signal, switch_state, bits + return selected_index--[[@as uint]], params.first_signal, switch_state, bits end ---@param comb LuaEntity ---@param is_pr_state 0|1|2 diff --git a/cybersyn/scripts/global.lua b/cybersyn/scripts/global.lua index da2c3dc..f11e453 100644 --- a/cybersyn/scripts/global.lua +++ b/cybersyn/scripts/global.lua @@ -104,6 +104,7 @@ --NOTE: any setting labeled as an "interface setting" can only be changed through the remote-interface, these settings are not save and have to be set at initialization --As a modder using the remote-interface, you may override any of these settings, including user settings. They will have to be overriden at initialization and whenever a user tries to change one. ---@class CybersynModSettings +---@field public enable_planner boolean ---@field public tps double ---@field public update_rate int ---@field public r_threshold int diff --git a/cybersyn/scripts/gui.lua b/cybersyn/scripts/gui.lua index 3a8e141..da54079 100644 --- a/cybersyn/scripts/gui.lua +++ b/cybersyn/scripts/gui.lua @@ -1,6 +1,5 @@ --By Mami -local flib_gui = require("__flib__.gui") -local flib_event = require("__flib__.event") +local flib_gui = require("__flib__.gui-lite") local RED = "utility/status_not_working" local GREEN = "utility/status_working" @@ -41,6 +40,7 @@ end local function set_visibility(main_window, selected_index) local is_station = selected_index == 1 local is_depot = selected_index == 2 + local is_wagon = selected_index == 5 local uses_network = is_station or is_depot or selected_index == 3 local uses_allow_list = is_station or selected_index == 3 @@ -56,107 +56,107 @@ local function set_visibility(main_window, selected_index) first_settings.allow_list.visible = uses_allow_list first_settings.is_stack.visible = is_station bottom_flow.enable_inactive.visible = is_station + top_flow.enable_slot_barring.visible = is_wagon depot_settings.visible = is_depot end ----@param comb LuaEntity ----@param player LuaPlayer -function gui_opened(comb, player) - combinator_update(global, comb, true) +---@param e EventData.on_gui_click +local function handle_close(e) + local element = e.element + if not element then return end + local comb = global.to_comb[element.tags.id] + if not comb or not comb.valid then return end + local player = game.get_player(e.player_index) + if not player then return end local rootgui = player.gui.screen - local selected_index, signal, switch_state, bits = get_comb_gui_settings(comb) - local window = flib_gui.build(rootgui, { - {type="frame", direction="vertical", ref={"main_window"}, name=COMBINATOR_NAME, children={ - --title bar - {type="flow", ref={"titlebar"}, children={ - {type="label", style="frame_title", caption={"cybersyn-gui.combinator-title"}, elem_mods={ignored_by_interaction=true}}, - {type="empty-widget", style="flib_titlebar_drag_handle", elem_mods={ignored_by_interaction=true}}, - {type="sprite-button", style="frame_action_button", mouse_button_filter={"left"}, sprite="utility/close_white", hovered_sprite="utility/close_black", name=COMBINATOR_NAME, actions={ - on_click = {"close", comb.unit_number} - }} - }}, - {type="frame", name="frame", style="inside_shallow_frame_with_padding", style_mods={padding=12, bottom_padding=9}, children={ - {type="flow", name="vflow", direction="vertical", style_mods={horizontal_align="left"}, children={ - --status - {type="flow", style="status_flow", direction="horizontal", style_mods={vertical_align="center", horizontally_stretchable=true, bottom_padding=4}, children={ - {type="sprite", sprite=STATUS_SPRITES[comb.status] or STATUS_SPRITES_DEFAULT, style="status_image", style_mods={stretch_image_to_widget_size=true}}, - {type="label", caption={STATUS_NAMES[comb.status] or STATUS_NAMES_DEFAULT}} - }}, - --preview - {type="frame", style="deep_frame_in_shallow_frame", style_mods={minimal_width=0, horizontally_stretchable=true, padding=0}, children={ - {type="entity-preview", style="wide_entity_button", ref={"preview"}}, - }}, - --drop down - {type="label", style="heading_3_label", caption={"cybersyn-gui.operation"}, style_mods={top_padding=8}}, - {type="flow", name="top", direction="horizontal", style_mods={vertical_align="center"}, children={ - {type="drop-down", style_mods={top_padding=3, right_margin=8}, ref={"operation"}, actions={ - on_selection_state_changed={"drop-down", comb.unit_number} - }, selected_index=selected_index, items={ - {"cybersyn-gui.comb1"}, - {"cybersyn-gui.depot"}, - {"cybersyn-gui.refueler"}, - {"cybersyn-gui.comb2"}, - {"cybersyn-gui.wagon-manifest"}, - }}, - {type="switch", name="is_pr_switch", ref={"is_pr_switch"}, allow_none_state=true, switch_state=switch_state, left_label_caption={"cybersyn-gui.switch-provide"}, right_label_caption={"cybersyn-gui.switch-request"}, left_label_tooltip={"cybersyn-gui.switch-provide-tooltip"}, right_label_tooltip={"cybersyn-gui.switch-request-tooltip"}, actions={ - on_switch_state_changed={"is_pr_switch", comb.unit_number} - }} - }}, - ---choose-elem-button - {type="line", style_mods={top_padding=10}}, - {type="label", name="network_label", style="heading_3_label", caption={"cybersyn-gui.network"}, style_mods={top_padding=8}}, - {type="flow", name="bottom", direction="horizontal", style_mods={vertical_align="top"}, children={ - {type="choose-elem-button", name="network", style="slot_button_in_shallow_frame", elem_type="signal", tooltip={"cybersyn-gui.network-tooltip"}, signal=signal, style_mods={bottom_margin=1, right_margin=6, top_margin=2}, actions={ - on_elem_changed={"choose-elem-button", comb.unit_number} - }}, - {type="flow", name="depot", direction="vertical", style_mods={horizontal_align="left"}, children={ - {type="flow", name="use_any_depot", direction="horizontal", style_mods={vertical_align="center"}, children={ - {type="checkbox", name="use_same_depot", state=setting_flip(bits, SETTING_USE_ANY_DEPOT), tooltip={"cybersyn-gui.use-same-depot-tooltip"}, actions={ - on_checked_state_changed={"setting-flip", comb.unit_number, SETTING_USE_ANY_DEPOT} - }}, - {type="label", name="use_same_depot_label", style_mods={left_padding=3}, caption={"cybersyn-gui.use-same-depot-description"}}, - }}, - {type="flow", name="depot_bypass", direction="horizontal", style_mods={vertical_align="center"}, children={ - {type="checkbox", name="depot_bypass", state=setting_flip(bits, SETTING_DISABLE_DEPOT_BYPASS), tooltip={"cybersyn-gui.depot-bypass-tooltip"}, actions={ - on_checked_state_changed={"setting-flip", comb.unit_number, SETTING_DISABLE_DEPOT_BYPASS} - }}, - {type="label", name="depot_bypass_label", style_mods={left_padding=3}, caption={"cybersyn-gui.depot-bypass-description"}}, - }}, - }}, - {type="flow", name="first", direction="vertical", style_mods={horizontal_align="left", right_margin=8}, children={ - {type="flow", name="allow_list", direction="horizontal", style_mods={vertical_align="center"}, children={ - {type="checkbox", name="allow_list", state=setting_flip(bits, SETTING_DISABLE_ALLOW_LIST), tooltip={"cybersyn-gui.allow-list-tooltip"}, actions={ - on_checked_state_changed={"setting-flip", comb.unit_number, SETTING_DISABLE_ALLOW_LIST} - }}, - {type="label", name="allow_list_label", style_mods={left_padding=3}, caption={"cybersyn-gui.allow-list-description"}}, - }}, - {type="flow", name="is_stack", direction="horizontal", style_mods={vertical_align="center"}, children={ - {type="checkbox", name="is_stack", state=setting(bits, SETTING_IS_STACK), tooltip={"cybersyn-gui.is-stack-tooltip"}, actions={ - on_checked_state_changed={"setting", comb.unit_number, SETTING_IS_STACK} - }}, - {type="label", name="is_stack_label", style_mods={left_padding=3}, caption={"cybersyn-gui.is-stack-description"}}, - }}, - }}, - {type="flow", name="enable_inactive", direction="horizontal", style_mods={vertical_align="center"}, children={ - {type="checkbox", name="enable_inactive", state=setting(bits, SETTING_ENABLE_INACTIVE), tooltip={"cybersyn-gui.enable-inactive-tooltip"}, actions={ - on_checked_state_changed={"setting", comb.unit_number, SETTING_ENABLE_INACTIVE} - }}, - {type="label", name="enable_inactive_label", style_mods={left_padding=3}, caption={"cybersyn-gui.enable-inactive-description"}}, - }}, - }} - }} - }} - }} - }) + if rootgui[COMBINATOR_NAME] then + rootgui[COMBINATOR_NAME].destroy() + player.play_sound({path = COMBINATOR_CLOSE_SOUND}) + end +end +---@param e EventData.on_gui_selection_state_changed +local function handle_drop_down(e) + local element = e.element + if not element then return end + local comb = global.to_comb[element.tags.id] + if not comb or not comb.valid then return end - window.preview.entity = comb - window.titlebar.drag_target = window.main_window - window.main_window.force_auto_center() + set_visibility(element.parent.parent.parent.parent, element.selected_index) - set_visibility(window.main_window, selected_index) - player.opened = window.main_window + if element.selected_index == 1 then + set_comb_operation(comb, MODE_PRIMARY_IO) + elseif element.selected_index == 2 then + set_comb_operation(comb, MODE_DEPOT) + --prevent the use of the each signal with depots + local network = element.parent.parent.bottom.network--[[@as LuaGuiElement]] + local signal = network.elem_value--[[@as SignalID]] + if signal.name == NETWORK_EACH then + network.elem_value = nil + set_comb_network_name(comb, nil) + end + elseif element.selected_index == 3 then + set_comb_operation(comb, MODE_REFUELER) + elseif element.selected_index == 4 then + set_comb_operation(comb, MODE_SECONDARY_IO) + elseif element.selected_index == 5 then + set_comb_operation(comb, MODE_WAGON) + else + return + end + + combinator_update(global, comb) +end +---@param e EventData.on_gui_switch_state_changed +local function handle_pr_switch(e) + local element = e.element + if not element then return end + local comb = global.to_comb[element.tags.id] + if not comb or not comb.valid then return end + + local is_pr_state = (element.switch_state == "none" and 0) or (element.switch_state == "left" and 1) or 2 + set_comb_is_pr_state(comb, is_pr_state) + + combinator_update(global, comb) +end +---@param e EventData.on_gui_elem_changed +local function handle_network(e) + local element = e.element + if not element then return end + local comb = global.to_comb[element.tags.id] + if not comb or not comb.valid then return end + + local signal = element.elem_value--[[@as SignalID]] + if signal and (signal.name == "signal-everything" or signal.name == "signal-anything" or signal.name == "signal-each") then + signal.name = NETWORK_EACH + element.elem_value = signal + end + set_comb_network_name(comb, signal) + + combinator_update(global, comb) +end +---@param e EventData.on_gui_checked_state_changed +local function handle_setting(e) + local element = e.element + if not element then return end + local comb = global.to_comb[element.tags.id] + if not comb or not comb.valid then return end + + set_comb_setting(comb, element.tags.bit--[[@as int]], element.state) + + combinator_update(global, comb) +end +---@param e EventData.on_gui_checked_state_changed +local function handle_setting_flip(e) + local element = e.element + if not element then return end + local comb = global.to_comb[element.tags.id] + if not comb or not comb.valid then return end + + set_comb_setting(comb, element.tags.bit--[[@as int]], not element.state) + + combinator_update(global, comb) end local function on_gui_opened(event) @@ -180,94 +180,103 @@ local function on_gui_closed(event) end end + function register_gui_actions() - flib_gui.hook_events(function(event) - local msg = flib_gui.read_action(event) - if msg then - local player = game.get_player(event.player_index) - if not player then return end - local rootgui = player.gui.screen - -- read the action to determine what to do - if msg[1] == "close" then - if rootgui[COMBINATOR_NAME] then - rootgui[COMBINATOR_NAME].destroy() - player.play_sound({path = COMBINATOR_CLOSE_SOUND}) - end - elseif msg[1] == "drop-down" then - local element = event.element - if not element then return end - local comb = global.to_comb[msg[2]] - if not comb or not comb.valid then return end - - set_visibility(rootgui[COMBINATOR_NAME], element.selected_index) - - if element.selected_index == 1 then - set_comb_operation(comb, MODE_PRIMARY_IO) - elseif element.selected_index == 2 then - set_comb_operation(comb, MODE_DEPOT) - --prevent the use of the each signal with depots - local network = element.parent.parent.bottom.network - local signal = network.elem_value - if signal.name == NETWORK_EACH then - network.elem_value = nil - set_comb_network_name(comb, nil) - end - elseif element.selected_index == 3 then - set_comb_operation(comb, MODE_REFUELER) - elseif element.selected_index == 4 then - set_comb_operation(comb, MODE_SECONDARY_IO) - elseif element.selected_index == 5 then - set_comb_operation(comb, MODE_WAGON) - else - return - end - - combinator_update(global, comb) - elseif msg[1] == "choose-elem-button" then - local element = event.element - if not element then return end - local comb = global.to_comb[msg[2]] - if not comb or not comb.valid then return end - - local signal = element.elem_value - if signal and (signal.name == "signal-everything" or signal.name == "signal-anything" or signal.name == "signal-each") then - signal.name = NETWORK_EACH - element.elem_value = signal - end - set_comb_network_name(comb, signal) - - combinator_update(global, comb) - elseif msg[1] == "setting" then - local element = event.element - if not element then return end - local comb = global.to_comb[msg[2]] - if not comb or not comb.valid then return end - - set_comb_setting(comb, msg[3], element.state) - - combinator_update(global, comb) - elseif msg[1] == "setting-flip" then - local element = event.element - if not element then return end - local comb = global.to_comb[msg[2]] - if not comb or not comb.valid then return end - - set_comb_setting(comb, msg[3], not element.state) - - combinator_update(global, comb) - elseif msg[1] == "is_pr_switch" then - local element = event.element - if not element then return end - local comb = global.to_comb[msg[2]] - if not comb or not comb.valid then return end - - local is_pr_state = (element.switch_state == "none" and 0) or (element.switch_state == "left" and 1) or 2 - set_comb_is_pr_state(comb, is_pr_state) - - combinator_update(global, comb) - end - end - end) - flib_event.register(defines.events.on_gui_opened, on_gui_opened) - flib_event.register(defines.events.on_gui_closed, on_gui_closed) + flib_gui.add_handlers({ + ["close"] = handle_close, + ["drop_down"] = handle_drop_down, + ["pr_switch"] = handle_pr_switch, + ["network"] = handle_network, + ["setting"] = handle_setting, + ["setting_flip"] = handle_setting_flip, + }) + script.on_event(defines.events.on_gui_opened, on_gui_opened) + script.on_event(defines.events.on_gui_closed, on_gui_closed) + flib_gui.handle_events() +end + +---@param comb LuaEntity +---@param player LuaPlayer +function gui_opened(comb, player) + combinator_update(global, comb, true) + + local rootgui = player.gui.screen + local selected_index, signal, switch_state, bits = get_comb_gui_settings(comb) + + local _, main_window = flib_gui.add(rootgui, { + {type="frame", direction="vertical", name=COMBINATOR_NAME, children={ + --title bar + {type="flow", name="titlebar", children={ + {type="label", style="frame_title", caption={"cybersyn-gui.combinator-title"}, elem_mods={ignored_by_interaction=true}}, + {type="empty-widget", style="flib_titlebar_drag_handle", elem_mods={ignored_by_interaction=true}}, + {type="sprite-button", style="frame_action_button", mouse_button_filter={"left"}, sprite="utility/close_white", hovered_sprite="utility/close_black", name=COMBINATOR_NAME, handler=handle_close, tags={id=comb.unit_number}} + }}, + {type="frame", name="frame", style="inside_shallow_frame_with_padding", style_mods={padding=12, bottom_padding=9}, children={ + {type="flow", name="vflow", direction="vertical", style_mods={horizontal_align="left"}, children={ + --status + {type="flow", style="status_flow", direction="horizontal", style_mods={vertical_align="center", horizontally_stretchable=true, bottom_padding=4}, children={ + {type="sprite", sprite=STATUS_SPRITES[comb.status] or STATUS_SPRITES_DEFAULT, style="status_image", style_mods={stretch_image_to_widget_size=true}}, + {type="label", caption={STATUS_NAMES[comb.status] or STATUS_NAMES_DEFAULT}} + }}, + --preview + {type="frame", name="preview_frame", style="deep_frame_in_shallow_frame", style_mods={minimal_width=0, horizontally_stretchable=true, padding=0}, children={ + {type="entity-preview", name="preview", style="wide_entity_button"}, + }}, + --drop down + {type="label", style="heading_3_label", caption={"cybersyn-gui.operation"}, style_mods={top_padding=8}}, + {type="flow", name="top", direction="horizontal", style_mods={vertical_align="center"}, children={ + {type="drop-down", style_mods={top_padding=3, right_margin=8}, handler=handle_drop_down, tags={id=comb.unit_number}, selected_index=selected_index, items={ + {"cybersyn-gui.comb1"}, + {"cybersyn-gui.depot"}, + {"cybersyn-gui.refueler"}, + {"cybersyn-gui.comb2"}, + {"cybersyn-gui.wagon-manifest"}, + }}, + {type="switch", name="is_pr_switch", allow_none_state=true, switch_state=switch_state, left_label_caption={"cybersyn-gui.switch-provide"}, right_label_caption={"cybersyn-gui.switch-request"}, left_label_tooltip={"cybersyn-gui.switch-provide-tooltip"}, right_label_tooltip={"cybersyn-gui.switch-request-tooltip"}, handler=handle_pr_switch, tags={id=comb.unit_number}}, + {type="flow", name="enable_slot_barring", direction="horizontal", style_mods={vertical_align="center", left_padding=6}, children={ + {type="checkbox", name="enable_slot_barring", state=setting(bits, SETTING_ENABLE_SLOT_BARRING), handler=handle_setting, tags={id=comb.unit_number, bit=SETTING_ENABLE_SLOT_BARRING}, tooltip={"cybersyn-gui.enable-slot-barring-tooltip"}}, + {type="label", name="enable_slot_barring_label", style_mods={left_padding=3}, caption={"cybersyn-gui.enable-slot-barring-description"}}, + }}, + }}, + ---choose-elem-button + {type="line", style_mods={top_padding=10}}, + {type="label", name="network_label", style="heading_3_label", caption={"cybersyn-gui.network"}, style_mods={top_padding=8}}, + {type="flow", name="bottom", direction="horizontal", style_mods={vertical_align="top"}, children={ + {type="choose-elem-button", name="network", style="slot_button_in_shallow_frame", elem_type="signal", tooltip={"cybersyn-gui.network-tooltip"}, signal=signal, style_mods={bottom_margin=1, right_margin=6, top_margin=2}, handler=handle_network, tags={id=comb.unit_number}}, + {type="flow", name="depot", direction="vertical", style_mods={horizontal_align="left"}, children={ + {type="flow", name="use_any_depot", direction="horizontal", style_mods={vertical_align="center"}, children={ + {type="checkbox", name="use_same_depot", state=setting_flip(bits, SETTING_USE_ANY_DEPOT), handler=handle_setting_flip, tags={id=comb.unit_number, bit=SETTING_USE_ANY_DEPOT}, tooltip={"cybersyn-gui.use-same-depot-tooltip"}}, + {type="label", name="use_same_depot_label", style_mods={left_padding=3}, caption={"cybersyn-gui.use-same-depot-description"}}, + }}, + {type="flow", name="depot_bypass", direction="horizontal", style_mods={vertical_align="center"}, children={ + {type="checkbox", name="depot_bypass", state=setting_flip(bits, SETTING_DISABLE_DEPOT_BYPASS), handler=handle_setting_flip, tags={id=comb.unit_number, bit=SETTING_DISABLE_DEPOT_BYPASS}, tooltip={"cybersyn-gui.depot-bypass-tooltip"}}, + {type="label", name="depot_bypass_label", style_mods={left_padding=3}, caption={"cybersyn-gui.depot-bypass-description"}}, + }}, + }}, + {type="flow", name="first", direction="vertical", style_mods={horizontal_align="left", right_margin=8}, children={ + {type="flow", name="allow_list", direction="horizontal", style_mods={vertical_align="center"}, children={ + {type="checkbox", name="allow_list", state=setting_flip(bits, SETTING_DISABLE_ALLOW_LIST), handler=handle_setting_flip, tags={id=comb.unit_number, bit=SETTING_DISABLE_ALLOW_LIST}, tooltip={"cybersyn-gui.allow-list-tooltip"}}, + {type="label", name="allow_list_label", style_mods={left_padding=3}, caption={"cybersyn-gui.allow-list-description"}}, + }}, + {type="flow", name="is_stack", direction="horizontal", style_mods={vertical_align="center"}, children={ + {type="checkbox", name="is_stack", state=setting(bits, SETTING_IS_STACK), handler=handle_setting, tags={id=comb.unit_number, bit=SETTING_IS_STACK}, tooltip={"cybersyn-gui.is-stack-tooltip"}}, + {type="label", name="is_stack_label", style_mods={left_padding=3}, caption={"cybersyn-gui.is-stack-description"}}, + }}, + }}, + {type="flow", name="enable_inactive", direction="horizontal", style_mods={vertical_align="center"}, children={ + {type="checkbox", name="enable_inactive", state=setting(bits, SETTING_ENABLE_INACTIVE), handler=handle_setting, tags={id=comb.unit_number, bit=SETTING_ENABLE_INACTIVE}, tooltip={"cybersyn-gui.enable-inactive-tooltip"}}, + {type="label", name="enable_inactive_label", style_mods={left_padding=3}, caption={"cybersyn-gui.enable-inactive-description"}}, + }}, + }} + }} + }} + }} + }) + + main_window.frame.vflow.preview_frame.preview.entity = comb + main_window.titlebar.drag_target = main_window + main_window.force_auto_center() + + set_visibility(main_window, selected_index) + player.opened = main_window end diff --git a/cybersyn/scripts/layout.lua b/cybersyn/scripts/layout.lua index e569f3a..a94cb2f 100644 --- a/cybersyn/scripts/layout.lua +++ b/cybersyn/scripts/layout.lua @@ -1,10 +1,11 @@ --By Mami -local area = require("__flib__.area") +local area = require("__flib__.bounding-box") local abs = math.abs local floor = math.floor local ceil = math.ceil local min = math.min local max = math.max +local bit_extract = bit32.extract ---@param layout_pattern (0|1|2|3)[] @@ -204,14 +205,15 @@ function set_p_wagon_combs(map_data, station, train) break end end - if carriage.type == "cargo-wagon" and item_i <= #manifest then + if carriage.type == "cargo-wagon" then local inv = carriage.get_inventory(defines.inventory.cargo_wagon) if inv then + ---@type ConstantCombinatorParameters local signals = {} local inv_filter_i = 1 local item_slots_capacity = max(ceil((#inv - locked_slots)*percent_slots_to_use_per_wagon), 1) - while item_slots_capacity > 0 do + while item_slots_capacity > 0 and item_i <= #manifest do local do_inc if item.type == "item" then local stack_size = get_stack_size(map_data, item.name) @@ -222,11 +224,13 @@ function set_p_wagon_combs(map_data, station, train) signals[i] = {index = i, signal = {type = item.type, name = item.name}, count = sign*count_to_fill} item_count = item_count - count_to_fill item_slots_capacity = item_slots_capacity - slots_to_fill - for j = 1, slots_to_fill do - inv.set_filter(inv_filter_i, item.name) - inv_filter_i = inv_filter_i + 1 + if comb then + for j = 1, slots_to_fill do + inv.set_filter(inv_filter_i, item.name) + inv_filter_i = inv_filter_i + 1 + end + train.has_filtered_wagon = true end - train.has_filtered_wagon = true do_inc = item_count == 0 else do_inc = true @@ -243,14 +247,18 @@ function set_p_wagon_combs(map_data, station, train) end if comb then + if bit_extract(get_comb_params(comb).second_constant, SETTING_ENABLE_SLOT_BARRING) > 0 then + inv.set_bar(inv_filter_i--[[@as uint]]) + train.has_filtered_wagon = true + end set_combinator_output(map_data, comb, signals) end end - elseif carriage.type == "fluid-wagon" and fluid_i <= #manifest then + elseif carriage.type == "fluid-wagon" then local fluid_capacity = carriage.prototype.fluid_capacity local signals = {} - while fluid_capacity > 0 do + while fluid_capacity > 0 and fluid_i <= #manifest do local do_inc if fluid.type == "fluid" then local count_to_fill = min(fluid_count, fluid_capacity) diff --git a/cybersyn/scripts/main.lua b/cybersyn/scripts/main.lua index 333ddba..abe9d72 100644 --- a/cybersyn/scripts/main.lua +++ b/cybersyn/scripts/main.lua @@ -189,12 +189,9 @@ local function search_for_station_combinator(map_data, stop, comb_operation, com {pos_x - 2, pos_y - 2}, {pos_x + 2, pos_y + 2} } - local entities = stop.surface.find_entities(search_area) + local entities = stop.surface.find_entities_filtered({area = search_area, name = COMBINATOR_NAME}) for _, entity in pairs(entities) do - if - entity.valid and entity.name == COMBINATOR_NAME and - entity ~= comb_forbidden and map_data.to_stop[entity.unit_number] == stop - then + if entity.valid and entity ~= comb_forbidden and map_data.to_stop[entity.unit_number] == stop then local param = get_comb_params(entity) if param.operation == comb_operation then return entity @@ -223,7 +220,7 @@ local function on_combinator_built(map_data, comb) end local stop = nil local rail = nil - local entities = comb.surface.find_entities(search_area) + local entities = comb.surface.find_entities_filtered({area = search_area, name = {"train-stop", "straight-rail"}}) for _, cur_entity in pairs(entities) do if cur_entity.valid then if cur_entity.name == "train-stop" then @@ -325,7 +322,7 @@ function on_combinator_broken(map_data, comb) if station then if station.entity_comb1 == comb then on_station_broken(map_data, id, station) - on_stop_built(map_data, stop, comb) + on_stop_built_or_updated(map_data, stop, comb) elseif station.entity_comb2 == comb then station.entity_comb2 = search_for_station_combinator(map_data, stop, MODE_SECONDARY_IO, comb) end @@ -334,13 +331,13 @@ function on_combinator_broken(map_data, comb) if depot then if depot.entity_comb == comb then on_depot_broken(map_data, id, depot) - on_stop_built(map_data, stop, comb) + on_stop_built_or_updated(map_data, stop, comb) end else local refueler = map_data.refuelers[id] if refueler and refueler.entity_comb == comb then on_refueler_broken(map_data, id, refueler) - on_stop_built(map_data, stop, comb) + on_stop_built_or_updated(map_data, stop, comb) end end end @@ -468,7 +465,8 @@ end ---@param map_data MapData ---@param stop LuaEntity ---@param comb_forbidden LuaEntity? -function on_stop_built(map_data, stop, comb_forbidden) +function on_stop_built_or_updated(map_data, stop, comb_forbidden) + --NOTE: this stop must not be a part of any station before entering this function local pos_x = stop.position.x local pos_y = stop.position.y @@ -480,20 +478,24 @@ function on_stop_built(map_data, stop, comb_forbidden) local comb1 = nil local depot_comb = nil local refueler_comb = nil - local entities = stop.surface.find_entities(search_area) + local entities = stop.surface.find_entities_filtered({area = search_area, name = COMBINATOR_NAME}) for _, entity in pairs(entities) do - if entity.valid and entity ~= comb_forbidden and entity.name == COMBINATOR_NAME and map_data.to_stop[entity.unit_number] == nil then - map_data.to_stop[entity.unit_number] = stop - local param = get_comb_params(entity) - local op = param.operation - if op == MODE_PRIMARY_IO then - comb1 = entity - elseif op == MODE_SECONDARY_IO then - comb2 = entity - elseif op == MODE_DEPOT then - depot_comb = entity - elseif op == MODE_REFUELER then - refueler_comb = entity + if entity.valid and entity ~= comb_forbidden then + local id = entity.unit_number--[[@as uint]] + local adj_stop = map_data.to_stop[id] + if adj_stop == nil or adj_stop == stop then + map_data.to_stop[id] = stop + local param = get_comb_params(entity) + local op = param.operation + if op == MODE_PRIMARY_IO then + comb1 = entity + elseif op == MODE_SECONDARY_IO then + comb2 = entity + elseif op == MODE_DEPOT then + depot_comb = entity + elseif op == MODE_REFUELER then + refueler_comb = entity + end end end end @@ -515,7 +517,7 @@ local function on_stop_broken(map_data, stop) {pos_x - 2, pos_y - 2}, {pos_x + 2, pos_y + 2} } - local entities = stop.surface.find_entities(search_area) + local entities = stop.surface.find_entities_filtered({area = search_area, name = COMBINATOR_NAME}) for _, entity in pairs(entities) do if entity.valid and map_data.to_stop[entity.unit_number] == stop then map_data.to_stop[entity.unit_number] = nil @@ -592,7 +594,7 @@ local function on_built(event) if not entity or not entity.valid then return end if entity.name == "train-stop" then - on_stop_built(global, entity) + on_stop_built_or_updated(global, entity) elseif entity.name == COMBINATOR_NAME then on_combinator_built(global, entity) elseif entity.type == "inserter" then @@ -778,6 +780,7 @@ end local function grab_all_settings() + mod_settings.enable_planner = settings.global["cybersyn-enable-planner"].value --[[@as boolean]] mod_settings.tps = settings.global["cybersyn-ticks-per-second"].value --[[@as double]] mod_settings.update_rate = settings.global["cybersyn-update-rate"].value --[[@as int]] mod_settings.r_threshold = settings.global["cybersyn-request-threshold"].value--[[@as int]] @@ -845,6 +848,15 @@ local function main() script.on_event(defines.events.on_entity_settings_pasted, on_paste) + script.on_event(defines.events.on_train_created, on_train_built) + script.on_event(defines.events.on_train_changed_state, on_train_changed) + + script.on_event(defines.events.on_entity_renamed, on_rename) + + script.on_event(defines.events.on_runtime_mod_setting_changed, on_settings_changed) + + register_gui_actions() + if mod_settings.tps > DELTA then local nth_tick = ceil(60/mod_settings.tps)--[[@as uint]]; script.on_nth_tick(nth_tick, function() @@ -854,14 +866,11 @@ local function main() script.on_nth_tick(nil) end - script.on_event(defines.events.on_train_created, on_train_built) - script.on_event(defines.events.on_train_changed_state, on_train_changed) - - script.on_event(defines.events.on_entity_renamed, on_rename) - - script.on_event(defines.events.on_runtime_mod_setting_changed, on_settings_changed) - - register_gui_actions() + script.on_event("cybersyn-toggle-planner", function(event) + local setting = settings.global["cybersyn-enable-planner"] + setting.value = not setting.value + settings.global["cybersyn-enable-planner"] = setting + end) script.on_init(function() local setting = settings.global["cybersyn-invert-sign"] diff --git a/cybersyn/scripts/migrations.lua b/cybersyn/scripts/migrations.lua index bdfae65..97b2e37 100644 --- a/cybersyn/scripts/migrations.lua +++ b/cybersyn/scripts/migrations.lua @@ -185,13 +185,15 @@ local migrations_table = { for train_id, train in pairs(map_data.trains) do train.depot_id = train.parked_at_depot_id if not train.depot_id then - local e = get_any_train_entity(train.entity) - local stops = e.force.get_train_stops({name = train.depot_name, surface = e.surface}) - for stop in rnext_consume, stops do - local new_depot_id = stop.unit_number - if map_data.depots[new_depot_id] then - train.depot_id = new_depot_id--[[@as uint]] - break + if train.entity.valid then + local e = get_any_train_entity(train.entity) + local stops = e.force.get_train_stops({name = train.depot_name, surface = e.surface}) + for stop in rnext_consume, stops do + local new_depot_id = stop.unit_number + if map_data.depots[new_depot_id] then + train.depot_id = new_depot_id--[[@as uint]] + break + end end end end @@ -200,7 +202,9 @@ local migrations_table = { end if not train.depot_id then train.entity.manual_mode = true - send_alert_depot_of_train_broken(map_data, train.entity) + if train.entity.valid then + send_alert_depot_of_train_broken(map_data, train.entity) + end local layout_id = train.layout_id local count = global.layout_train_count[layout_id] if count <= 1 then diff --git a/cybersyn/scripts/remote-interface.lua b/cybersyn/scripts/remote-interface.lua index 1f5bf73..169889f 100644 --- a/cybersyn/scripts/remote-interface.lua +++ b/cybersyn/scripts/remote-interface.lua @@ -115,6 +115,7 @@ end --[[helper functions]] ------------------------------------------------------------------ --NOTE: the policy of cybersyn is to give modders access to as much of the raw data of the mod as possible. Factorio only allows me to return copies of the original data rather than the actual thing, which sucks. The unsafe api has some tools to help you bypass this limitation. +--Some of these functions are so simplistic I'd recommend not even using them and just copy-pasting their internal code. function interface.get_mod_settings() return mod_settings @@ -246,12 +247,13 @@ end --[[unsafe API]] ------------------------------------------------------------------ --NOTE: The following functions can cause serious longterm damage to someone's world if they are given bad parameters. Please refer to global.lua for type information. Use caution. +--If there is any useful function missing from this API I'd be happy to add it. Join the Cybersyn discord to request it be added. ---@param value any ---@param ... string|int function interface.write_global(value, ...) --this can write anything into cybersyn's map_data, please be very careful with anything you write, it can cause permanent damage - --so interface.read_global(nil, "trains", 31415, "manifest") will cause global.trains[31415].manifest = nil (or return false if train 31415 does not exist) + --so interface.write_global(nil, "trains", 31415, "manifest") will cause global.trains[31415].manifest = nil (or return false if train 31415 does not exist) local params = {...} local size = #params local key = params[size] @@ -271,7 +273,7 @@ end function interface.remove_manifest_from_station_deliveries(station_id, manifest, sign) local station = global.stations[station_id] assert(station) - remove_manifest(global, station, manifest, sign) + return remove_manifest(global, station, manifest, sign) end ---@param r_station_id uint ---@param p_station_id uint @@ -279,7 +281,7 @@ end function interface.create_manifest(r_station_id, p_station_id, train_id) local train = global.trains[train_id] assert(global.stations[r_station_id] and global.stations[p_station_id] and train and train.is_available) - create_manifest(global, r_station_id, p_station_id, train_id) + return create_manifest(global, r_station_id, p_station_id, train_id) end ---@param r_station_id uint ---@param p_station_id uint @@ -288,19 +290,19 @@ end function interface.create_delivery(r_station_id, p_station_id, train_id, manifest) local train = global.trains[train_id] assert(global.stations[r_station_id] and global.stations[p_station_id] and train and train.is_available and manifest) - create_delivery(global, r_station_id, p_station_id, train_id, manifest) + return create_delivery(global, r_station_id, p_station_id, train_id, manifest) end ---@param train_id uint function interface.fail_delivery(train_id) local train = global.trains[train_id] assert(train) - on_failed_delivery(global, train_id, train) + return on_failed_delivery(global, train_id, train) end ---@param train_id uint function interface.remove_train(train_id) local train = global.trains[train_id] assert(train) - remove_train(global, train_id, train) + return remove_train(global, train_id, train) end ---@param train_id uint @@ -320,14 +322,14 @@ function interface.add_available_train_to_depot(train_id, depot_id) local train = global.trains[train_id] local depot = global.depots[depot_id] assert(train and depot) - add_available_train_to_depot(global, mod_settings, train_id, train, depot_id, depot) + return add_available_train_to_depot(global, mod_settings, train_id, train, depot_id, depot) end ---@param train_id uint function interface.remove_available_train(train_id) --this function removes a train from the available trains list so it cannot be rescheduled and dispatched. if the train was not already available nothing will happen local train = global.trains[train_id] assert(train) - remove_available_train(global, train_id, train) + return remove_available_train(global, train_id, train) end ------------------------------------------------------------------ diff --git a/cybersyn/scripts/train-events.lua b/cybersyn/scripts/train-events.lua index be043f0..b1afc4d 100644 --- a/cybersyn/scripts/train-events.lua +++ b/cybersyn/scripts/train-events.lua @@ -52,11 +52,12 @@ function on_failed_delivery(map_data, train_id, train) train.has_filtered_wagon = nil for carriage_i, carriage in ipairs(train.entity.cargo_wagons) do local inv = carriage.get_inventory(defines.inventory.cargo_wagon) - if inv and inv.is_filtered() then + if inv then ---@type uint - for i = 1, #inv do + for i = 1, inv.get_bar() - 1 do inv.set_filter(i, nil) end + inv.set_bar() end end end @@ -211,38 +212,19 @@ end ---@param train_id uint ---@param train Train local function on_train_arrives_station(map_data, station_id, train_id, train) - if train.manifest then - ---@type uint - if train.status == STATUS_TO_P then - if train.p_station_id == station_id then - train.status = STATUS_P - local station = map_data.stations[station_id] - set_comb1(map_data, station, train.manifest, mod_settings.invert_sign and 1 or -1) - set_p_wagon_combs(map_data, station, train) - interface_raise_train_status_changed(train_id, STATUS_TO_P, STATUS_P) - end - elseif train.status == STATUS_TO_R then - if train.r_station_id == station_id then - train.status = STATUS_R - local station = map_data.stations[station_id] - set_comb1(map_data, station, train.manifest, mod_settings.invert_sign and -1 or 1) - set_r_wagon_combs(map_data, station, train) - interface_raise_train_status_changed(train_id, STATUS_TO_R, STATUS_R) - end - elseif train.status == STATUS_P and train.p_station_id == station_id then - --this is player intervention that is considered valid - elseif (train.status == STATUS_R or train.status == STATUS_TO_D or train.status == STATUS_TO_D_BYPASS) and train.r_station_id == station_id then - --this is player intervention that is considered valid - elseif mod_settings.react_to_train_at_incorrect_station then - on_failed_delivery(map_data, train_id, train) - remove_train(map_data, train_id, train) - lock_train(train.entity) - send_alert_train_at_incorrect_station(map_data, train.entity) - end - elseif mod_settings.react_to_train_at_incorrect_station then - --train is lost somehow, probably from player intervention - remove_train(map_data, train_id, train) - send_alert_train_at_incorrect_station(map_data, train.entity) + ---@type uint + if train.status == STATUS_TO_P then + train.status = STATUS_P + local station = map_data.stations[station_id] + set_comb1(map_data, station, train.manifest, mod_settings.invert_sign and 1 or -1) + set_p_wagon_combs(map_data, station, train) + interface_raise_train_status_changed(train_id, STATUS_TO_P, STATUS_P) + elseif train.status == STATUS_TO_R then + train.status = STATUS_R + local station = map_data.stations[station_id] + set_comb1(map_data, station, train.manifest, mod_settings.invert_sign and -1 or 1) + set_r_wagon_combs(map_data, station, train) + interface_raise_train_status_changed(train_id, STATUS_TO_R, STATUS_R) end end @@ -274,11 +256,12 @@ local function on_train_leaves_stop(map_data, mod_settings, train_id, train) train.has_filtered_wagon = nil for carriage_i, carriage in ipairs(train.entity.cargo_wagons) do local inv = carriage.get_inventory(defines.inventory.cargo_wagon) - if inv and inv.is_filtered() then + if inv then ---@type uint - for i = 1, #inv do + for i = 1, inv.get_bar() - 1 do inv.set_filter(i, nil) end + inv.set_bar() end end end @@ -426,19 +409,21 @@ function on_train_built(event) end end function on_train_changed(event) + ---@type MapData + local map_data = global local train_e = event.train--[[@as LuaTrain]] if not train_e.valid then return end local train_id = train_e.id - if global.active_alerts then + if map_data.active_alerts then --remove the alert if the train is interacted with at all - local data = global.active_alerts[train_id] + local data = map_data.active_alerts[train_id] if data then --we need to wait for the train to come to a stop from being locked - if data[3] + 10*mod_settings.tps < global.total_ticks then - global.active_alerts[train_id] = nil - if next(global.active_alerts) == nil then - global.active_alerts = nil + if data[3] + 10*mod_settings.tps < map_data.total_ticks then + map_data.active_alerts[train_id] = nil + if next(map_data.active_alerts) == nil then + map_data.active_alerts = nil end end end @@ -448,24 +433,48 @@ function on_train_changed(event) local stop = train_e.station if stop and stop.valid and stop.name == "train-stop" then local id = stop.unit_number--[[@as uint]] - if global.stations[id] then - local train = global.trains[train_id] - if train then - on_train_arrives_station(global, id, train_id, train) - end - elseif global.depots[id] then - on_train_arrives_depot(global, id, train_e) - elseif global.refuelers[id] then - local train = global.trains[train_id] - if train then - on_train_arrives_refueler(global, id, train_id, train) + if map_data.depots[id] then + on_train_arrives_depot(map_data, id, train_e) + end + else + local train = map_data.trains[train_id] + if train then + local schedule = train_e.schedule + if schedule then + local rail = schedule.records[schedule.current].rail + if rail then + local id, station, is_station + if train.status == STATUS_TO_P then + id = train.p_station_id + station = map_data.stations[id] + is_station = true + elseif train.status == STATUS_TO_R then + id = train.r_station_id + station = map_data.stations[id] + is_station = true + elseif train.status == STATUS_TO_F then + id = train.refueler_id + station = map_data.refuelers[id] + is_station = false + end + if id and station.entity_stop.connected_rail == rail then + if is_station then + on_train_arrives_station(map_data, id, train_id, train) + else + on_train_arrives_refueler(map_data, id, train_id, train) + end + end + end end end end elseif event.old_state == defines.train_state.wait_station then - local train = global.trains[train_id] - if train then - on_train_leaves_stop(global, mod_settings, train_id, train) + local path = train_e.path + if path and path.total_distance > 4 then + local train = map_data.trains[train_id] + if train then + on_train_leaves_stop(map_data, mod_settings, train_id, train) + end end end end diff --git a/cybersyn/settings.lua b/cybersyn/settings.lua index 447eb13..91b6ac7 100644 --- a/cybersyn/settings.lua +++ b/cybersyn/settings.lua @@ -1,9 +1,16 @@ --By Mami data:extend({ + { + type = "bool-setting", + name = "cybersyn-enable-planner", + order = "aa", + setting_type = "runtime-global", + default_value = true, + }, { type = "double-setting", name = "cybersyn-ticks-per-second", - order = "aa", + order = "ab", setting_type = "runtime-global", default_value = 30, minimum_value = 0, @@ -12,7 +19,7 @@ data:extend({ { type = "int-setting", name = "cybersyn-update-rate", - order = "ab", + order = "ac", setting_type = "runtime-global", default_value = 2, minimum_value = 1,