mirror of
https://github.com/wesnoth/wesnoth
synced 2025-04-25 23:53:18 +00:00

Ensures that files either use tabs for indentation or spaces for indentation, but don't switch between the two within the same file. This doesn't fix the whitespace, it's a simple check to flag it up on the assumption that it's better to use an editor or code formatter to clean up the file. Elsewhere in the CI we use the luacheck tool - while that can detect mixing tabs and spaces in a single line's indent, it doesn't check for inconsistent indentation within a file.
542 lines
14 KiB
Lua
542 lines
14 KiB
Lua
local location_set = {}
|
|
|
|
local function index(x, y)
|
|
-- the 2000 bias ensure that the correct x is recovered for negative y
|
|
return x * 16384 + y + 2000
|
|
end
|
|
|
|
local invscale = 1 / 16384
|
|
local function revindex(p)
|
|
local x = math.floor(p * invscale)
|
|
return x, p - x * 16384 - 2000
|
|
end
|
|
|
|
---@alias location_set_operation fun(x:integer, y:integer, value:any):boolean
|
|
---@alias location_set_resolver fun(x:integer, y:integer, old:any, new:any):any
|
|
---A set of locations, with an optional associated value for each one.
|
|
---@class location_set
|
|
---@field values table<integer, any>
|
|
local methods = {}
|
|
local locset_meta = {}
|
|
|
|
function locset_meta:__call(x, y)
|
|
return self:get(x, y)
|
|
end
|
|
|
|
function locset_meta:__index(loc)
|
|
if type(loc) == 'string' then
|
|
return methods[loc]
|
|
elseif loc.x and loc.y then
|
|
return self:get(loc.x, loc.y)
|
|
else
|
|
return self:get(loc[1], loc[2])
|
|
end
|
|
end
|
|
|
|
function locset_meta:__newindex(loc, val)
|
|
local fcn = methods.insert
|
|
if val == nil then fcn = methods.remove end
|
|
if loc.x and loc.y then
|
|
fcn(self, loc.x, loc.y, val)
|
|
else
|
|
fcn(self, loc[1], loc[2], val)
|
|
end
|
|
end
|
|
|
|
function locset_meta:__bor(other)
|
|
local new = self:clone()
|
|
new:union(other)
|
|
return new
|
|
end
|
|
|
|
function locset_meta:__band(other)
|
|
local new = self:clone()
|
|
new:inter(other)
|
|
return new
|
|
end
|
|
|
|
function locset_meta:__bxor(other)
|
|
local new = self:clone()
|
|
new:symm(other)
|
|
return new
|
|
end
|
|
|
|
if wesnoth.current then
|
|
function locset_meta:__bnot(other)
|
|
return self:invert(wesnoth.current.map)
|
|
end
|
|
end
|
|
|
|
function locset_meta:__sub(other)
|
|
local new = self:clone()
|
|
new:diff(other)
|
|
return new
|
|
end
|
|
|
|
function locset_meta:__tostring()
|
|
local res = {}
|
|
self:iter(function(x, y, v) res[string.format('(%d,%d)', x, y)] = tostring(v) end)
|
|
return '{' .. stringx.join_map(res, '; ', ' = ') .. '}'
|
|
end
|
|
|
|
---Test if the set is empty
|
|
---@return boolean
|
|
function methods:empty()
|
|
return (not next(self.values))
|
|
end
|
|
|
|
---Count the number of locations in the set
|
|
---@return integer
|
|
function methods:size()
|
|
local sz = 0
|
|
for p,v in pairs(self.values) do
|
|
sz = sz + 1
|
|
end
|
|
return sz
|
|
end
|
|
|
|
---Remove all locations from the set
|
|
function methods:clear()
|
|
self.values = {}
|
|
end
|
|
|
|
---Look up a location in the set
|
|
---@overload fun(x:integer, y:integer):any
|
|
---@overload fun(loc:location):any
|
|
function methods:get(...)
|
|
local loc = wesnoth.map.read_location(...)
|
|
if loc ~= nil then
|
|
return self.values[index(loc.x, loc.y)]
|
|
end
|
|
return nil
|
|
end
|
|
|
|
---Add a location to the set
|
|
---@overload fun(set:location_set, x:integer, y:integer, value?:any)
|
|
---@overload fun(set:location_set, loc:location, value?:any)
|
|
function methods:insert(...)
|
|
local loc, n = wesnoth.map.read_location(...)
|
|
if loc ~= nil then
|
|
local v = select(n + 1, ...)
|
|
self.values[index(loc.x, loc.y)] = v or true
|
|
end
|
|
end
|
|
|
|
---Remove a location from the set
|
|
---@overload fun(x:integer, y:integer)
|
|
---@overload fun(loc:location|unit)
|
|
function methods:remove(...)
|
|
local loc = wesnoth.map.read_location(...)
|
|
if loc ~= nil then
|
|
self.values[index(loc.x, loc.y)] = nil
|
|
end
|
|
end
|
|
|
|
---Create a copy of the set
|
|
---This is a shallow copy - if the values are tables they will still
|
|
---be referenced by the original set
|
|
---@return location_set
|
|
function methods:clone()
|
|
local new = location_set.create()
|
|
for p,v in pairs(self.values) do
|
|
new.values[p] = v
|
|
end
|
|
return new
|
|
end
|
|
|
|
---Take the union of two sets, replacing duplicate values
|
|
---@param s location_set The other set to merge in
|
|
function methods:union(s)
|
|
local values = self.values
|
|
for p,v in pairs(s.values) do
|
|
values[p] = v
|
|
end
|
|
end
|
|
|
|
---Take the union of two sets, merging duplicate values
|
|
---@param s location_set The other set to merge in
|
|
---@param f location_set_resolver A function which is used to resolve conflicts.
|
|
---It will be called for every element of the right-hand set.
|
|
function methods:union_merge(s, f)
|
|
local values = self.values
|
|
for p,v in pairs(s.values) do
|
|
local x, y = revindex(p)
|
|
values[p] = f(x, y, values[p], v)
|
|
end
|
|
end
|
|
|
|
---Take the intersection of two sets, by removing any elements in the left set that
|
|
---are missing from the right set.
|
|
---@param s location_set The other set to intersect with
|
|
function methods:inter(s)
|
|
local values = self.values
|
|
local nvalues = {}
|
|
for p,v in pairs(s.values) do
|
|
nvalues[p] = values[p]
|
|
end
|
|
self.values = nvalues
|
|
end
|
|
|
|
---Take the union of two sets, merging duplicate values
|
|
---@param s location_set The other set to merge in
|
|
---@param f location_set_resolver A function which is used to resolve conflicts.
|
|
---It will be called for every element common to both sets.
|
|
function methods:inter_merge(s, f)
|
|
local values = s.values
|
|
local nvalues = {}
|
|
for p,v in pairs(self.values) do
|
|
local x, y = revindex(p)
|
|
nvalues[p] = f(x, y, v, values[p])
|
|
end
|
|
self.values = nvalues
|
|
end
|
|
|
|
---Take the relative complement of two location sets.
|
|
---This removes any elements from the left set that are also in the right set.
|
|
---@param s location_set
|
|
function methods:diff(s)
|
|
local values = self.values
|
|
for p,v in pairs(s.values) do
|
|
values[p] = nil
|
|
end
|
|
end
|
|
|
|
---Take the symmetric difference of two location sets.
|
|
---This removes any elements that are common to both sets,
|
|
---while adding new elements to the left set if they only exist in the right.
|
|
---@param s location_set
|
|
function methods:symm(s)
|
|
local values = self.values
|
|
for p,v in pairs(s.values) do
|
|
if values[p] then
|
|
values[p] = nil
|
|
else
|
|
values[p] = v
|
|
end
|
|
end
|
|
end
|
|
|
|
---Take the absolute complement of the location set
|
|
---The resulting set has no values associated with each location.
|
|
---@param width integer The width of the map
|
|
---@param height integer The height of the map
|
|
---@param border_size integer The border size of the map
|
|
---@return location_set
|
|
---@overload fun(map:terrain_map):location_set
|
|
function methods:invert(width, height, border_size)
|
|
if type(width) == 'number' and type(height) == 'number' then
|
|
border_size = border_size or 0
|
|
elseif type(width) == 'userdata' and getmetatable(width) == 'terrain map' then
|
|
local map = width
|
|
width = map.playable_width
|
|
height = map.playable_height
|
|
border_size = map.border_size
|
|
else
|
|
error('Invalid arguments to location_set:invert - expected a map or map dimensions', 2)
|
|
end
|
|
local new = location_set.create()
|
|
for x = 1 - border_size, width + border_size do
|
|
for y = 1 - border_size, height + border_size do
|
|
if not self:get(x, y) then
|
|
new:insert(x, y)
|
|
end
|
|
end
|
|
end
|
|
return new
|
|
end
|
|
|
|
---Filter the set for elements that satisfy a given condition.
|
|
---Returns a new set containing only the matching values.
|
|
---@param f location_set_operation The condition to test
|
|
---@return location_set
|
|
function methods:filter(f)
|
|
local nvalues = {}
|
|
for p,v in pairs(self.values) do
|
|
local x, y = revindex(p)
|
|
if f(x, y, v) then nvalues[p] = v end
|
|
end
|
|
return setmetatable({ values = nvalues }, locset_meta)
|
|
end
|
|
|
|
---Iterate over the location set.
|
|
---If passed no arguments, it can be used in a range-for loop.
|
|
---@param f location_set_operation
|
|
---@overload fun():fun()
|
|
function methods:iter(f)
|
|
if f == nil then
|
|
local locs = self
|
|
return coroutine.wrap(function()
|
|
locs:iter(coroutine.yield)
|
|
end)
|
|
end
|
|
for p,v in pairs(self.values) do
|
|
local x, y = revindex(p)
|
|
f(x, y, v)
|
|
end
|
|
end
|
|
|
|
---Iterate over the location set in a sorted order.
|
|
---If passed no arguments, it can be used in a range-for loop.
|
|
---@param f location_set_operation
|
|
---@overload fun():fun()
|
|
function methods:stable_iter(f)
|
|
if f == nil then
|
|
local locs = self
|
|
return coroutine.wrap(function()
|
|
locs:stable_iter(coroutine.yield)
|
|
end)
|
|
end
|
|
local indices = {}
|
|
for p,v in pairs(self.values) do
|
|
table.insert(indices, p)
|
|
end
|
|
table.sort(indices)
|
|
for i,p in ipairs(indices) do
|
|
local x, y = revindex(p)
|
|
f(x, y, self.values[p])
|
|
end
|
|
end
|
|
|
|
---Add a series of locations to the set.
|
|
---If the location tables have extra information in them, it will be used as the value.
|
|
---@param t location[]
|
|
function methods:of_pairs(t)
|
|
local values = self.values
|
|
|
|
local function has_key(v, k)
|
|
if type(v) == 'userdata' then
|
|
return v[k] ~= nil
|
|
elseif type(v) == 'table' then
|
|
return rawget(v, k) ~= nil
|
|
end
|
|
end
|
|
|
|
for i,v in ipairs(t) do
|
|
local value_table = {}
|
|
local x_index
|
|
local y_index
|
|
if has_key(v, 'x') and has_key(v, 'y') then
|
|
x_index = "x"
|
|
y_index = "y"
|
|
else
|
|
x_index = 1
|
|
y_index = 2
|
|
end
|
|
for k,val in pairs(v) do
|
|
if k ~= x_index and k ~= y_index then
|
|
value_table[k] = val
|
|
end
|
|
end
|
|
if next(value_table) then
|
|
values[index(v[x_index], v[y_index])] = value_table
|
|
else
|
|
values[index(v[x_index], v[y_index])] = true
|
|
end
|
|
end
|
|
end
|
|
|
|
---Add locations stored in a WML variable.
|
|
---If the location tables have extra information in them, it will be used as the value.
|
|
---@param name string
|
|
function methods:of_wml_var(name)
|
|
local values = self.values
|
|
for i = 0, wml.variables[name .. ".length"] - 1 do
|
|
local t = wml.variables[string.format("%s[%d]", name, i)]
|
|
local x, y = t.x, t.y
|
|
t.x, t.y = nil, nil
|
|
values[index(x, y)] = next(t) and t or true
|
|
end
|
|
end
|
|
|
|
---Add a series of locations to the set.
|
|
---The third element of each location is used as the value.
|
|
---@param t location_triple[]
|
|
function methods:of_triples(t)
|
|
-- Create a location set from a table of 3-element tables
|
|
-- Elements 1 and 2 are x,y coordinates, #3 is value to be inserted
|
|
for k,v in pairs(t) do
|
|
if #v == 0 then
|
|
self:insert(v.x, v.y, v.value)
|
|
else
|
|
self:insert(v[1], v[2], v[3])
|
|
end
|
|
end
|
|
end
|
|
|
|
--- Add values from a table of location->element mappings
|
|
--- Keys can be of the form {x,y} or {x=x,y=y}, or a location-like object such as a unit
|
|
---@param t table<location, any>
|
|
function methods:of_map(t)
|
|
for k,v in pairs(t) do
|
|
local loc = wesnoth.read_location(k)
|
|
self:insert(loc.x, loc.y, v)
|
|
end
|
|
end
|
|
|
|
---Add a series of locations from a shroud data string.
|
|
---Each location indicated by a 1 will be added to the set.
|
|
---@param data string
|
|
function methods:of_shroud_data(data)
|
|
self:of_pairs(wesnoth.map.parse_bitmap(data))
|
|
end
|
|
|
|
---Convert the set to an array of locations.
|
|
---The value will not be stored in the output array.
|
|
---@return location[]
|
|
function methods:to_pairs()
|
|
local res = {}
|
|
self:iter(function(x, y)
|
|
table.insert(res, wesnoth.named_tuple({ x, y }, {'x', 'y'}))
|
|
end)
|
|
return res
|
|
end
|
|
|
|
---Convert the set to an array of locations in a sorted order.
|
|
---The value will not be stored in the output array.
|
|
---@return location[]
|
|
function methods:to_stable_pairs()
|
|
local res = {}
|
|
self:stable_iter(function(x, y)
|
|
table.insert(res, wesnoth.named_tuple({ x, y }, {'x', 'y'}))
|
|
end)
|
|
return res
|
|
end
|
|
|
|
---Store the set in a WML variable
|
|
---@param name string
|
|
---@param mode "'always_clear'"|"'append'"|"'replace'"
|
|
function methods:to_wml_var(name, mode)
|
|
mode = mode or "always_clear"
|
|
local is_explicit_index = name[-1] == "]"
|
|
local i = 0
|
|
-- explicit indexes behave always like "replace"
|
|
if not is_explicit_index then
|
|
if mode == "append" then
|
|
i = wml.variables[name .. ".length"]
|
|
elseif mode ~= "replace" then
|
|
wml.variables[name] = nil
|
|
end
|
|
end
|
|
self:stable_iter(function(x, y, v)
|
|
if wml.valid(v) then
|
|
wml.variables[string.format("%s[%d]", name, i)] = v
|
|
elseif wml.valid{value = v} then
|
|
wml.variables[string.format("%s[%d]", name, i)] = {value = v}
|
|
elseif type(v) ~= 'boolean' then
|
|
warning('Location set value could not be converted to a WML variable:', v)
|
|
end
|
|
wml.variables[string.format("%s[%d].x", name, i)] = x
|
|
wml.variables[string.format("%s[%d].y", name, i)] = y
|
|
i = i + 1
|
|
end)
|
|
end
|
|
|
|
---Convert the set to an array of triples - locations with an extra element for the value.
|
|
---@return location_triple[]
|
|
function methods:to_triples()
|
|
local res = {}
|
|
self:iter(function(x, y, v)
|
|
table.insert(res, wesnoth.named_tuple({ x, y, v }, {"x", "y", "value"}))
|
|
end)
|
|
return res
|
|
end
|
|
|
|
---Convert the set to a map of location -> value
|
|
---@return table<location, any>
|
|
function methods:to_map()
|
|
local res = {}
|
|
self:iter(function(x, y, v)
|
|
res[wesnoth.named_tuple({x, y}, {"x", "y"})] = v
|
|
end)
|
|
return res
|
|
end
|
|
|
|
---Convert the set to a shroud data string
|
|
--- Each location in the set will be a 1 in the output string.
|
|
---@return string
|
|
function methods:to_shroud_data()
|
|
return wesnoth.map.make_bitmap(self:to_pairs())
|
|
end
|
|
|
|
---Select a random location from the set
|
|
---@return integer #x
|
|
---@return integer #y
|
|
function methods:random()
|
|
-- Select a random hex from the hexes in the location set
|
|
-- This seems "inelegant", but I can't come up with another way without creating an extra array
|
|
-- Return -1, -1 if empty
|
|
local r = mathx.random(self:size())
|
|
local i, xr, yr = 1, -1, -1
|
|
self:iter( function(x, y, v)
|
|
if (i == r) then xr, yr = x, y end
|
|
i = i + 1
|
|
end)
|
|
return xr, yr
|
|
end
|
|
|
|
---Create a new empty location set.
|
|
---@return location_set
|
|
function location_set.create()
|
|
return setmetatable({ values = {} }, locset_meta)
|
|
end
|
|
|
|
---Create a location set from raw indexed data.
|
|
---The data must be formatted the same way as the values of a location set.
|
|
---@param data table<integer, any>
|
|
---@return location_set
|
|
function location_set.of_raw(data)
|
|
return setmetatable({ values = data }, locset_meta)
|
|
end
|
|
|
|
---Create a set with locations from an array.
|
|
---If the location tables have extra information in them, it will be used as the value.
|
|
---@param t location[]
|
|
---@return location_set
|
|
function location_set.of_pairs(t)
|
|
local s = location_set.create()
|
|
s:of_pairs(t)
|
|
return s
|
|
end
|
|
|
|
---Create a set with locations from a WML variable.
|
|
---If the location tables have extra information in them, it will be used as the value.
|
|
---@param name string
|
|
---@return location_set
|
|
function location_set.of_wml_var(name)
|
|
local s = location_set.create()
|
|
s:of_wml_var(name)
|
|
return s
|
|
end
|
|
|
|
---Create a set with locations from an array.
|
|
---The third element of each location is used as the value.
|
|
---@param t location_triple[]
|
|
---@return location_set
|
|
function location_set.of_triples(t)
|
|
local s = location_set.create()
|
|
s:of_triples(t)
|
|
return s
|
|
end
|
|
|
|
--- Create a set from a table of location->element mappings
|
|
--- Keys can be of the form {x,y} or {x=x,y=y}, or a location-like object such as a unit
|
|
---@param t table<location, any>
|
|
---@return location_set
|
|
function location_set.of_map(t)
|
|
local s = location_set.create()
|
|
s:of_map(t)
|
|
return s
|
|
end
|
|
|
|
---Create a set with locations from a shroud data string.
|
|
---Each location indicated by a 1 will be added to the set.
|
|
---@param data string
|
|
---@return location_set
|
|
function location_set.of_shroud_data(data)
|
|
local s = location_set.create()
|
|
s:of_shroud_data(data)
|
|
return s
|
|
end
|
|
|
|
return location_set
|