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