Module:Test

From RimWorld Wiki
Jump to navigation Jump to search

This module is used for development. Even if it works at the moment it might break at any time.

Most of the data should be available (ThingDefs and BiomeDefs look complete, if there's need for other Defs - easily added). Parser is done (needs a few more finishing touches) so any data that is missing can be easily added.

Description

This module is used to get information about game objects. Its main purpose is to populate the contents of infoboxes.

Values retrieved are those defined in the files so they may need to be processed to be user friendly. For example "foodType" that holds the value "OmnivoreRoughAnimal" is not entirely helpful. (TODO: This could be automatically converted to something more recognizable.)

Usage

query

{{#invoke:Test|query|<defName>|...|<tag>|<sibling=...>}}
<defName>
defName of the Def. Works for abstract Defs but in that case you have to give it the "Name" attribute.
...
Additional arguments are here to help uniquely identify the final argument <tag>. If the wanted tag is already unique within a Def tree, then additional parameters are not needed.
<tag>
This is the key to be retrieved. They are the same as those in the XML files (filtered, of course; not all of the data from the files is available).
<sibling=...>
Allows querying for something if we know a sibling value (works only for values at the moment, querying by keys or key/value pairs will most likely be added). It does not have to be at the end because named arguments do not depend on the order. (Might move this functionality to a separate function if the need arises).

The algorithm will return the first value found. If the query generates a list, for now, the default behaviour is to dump the contents of the thing to the log. The log can be accessed when previewing a page you're editing. A hint that this has happened (there is something in the log) is that the module returned the string "table".

Note for abstract Defs: they will automatically get merged with their parents.

count

{{#invoke:Test|count|<defName>|...|<tag>|<sibling=...>}}

Arguments are the same as for #query. It's basically a wrapped up query.

Because count (if not 0) means that a table was counted - it will also be displayed in the log.

getDefName

{{#invoke:Test|getDefName|<label>}}
<label>
get defName based on the label of a Def (not case sensitive)

This doesn't work if (for any reason you migh have) you want to get abstract (parent/inheritable) Defs because they don't have a <label>.

Examples

complete contents of a Def

This is not the complete set as the one available to the game itself. Because a "table" is returned, the contents are in the log.

{{#invoke:Test|query|Hare}}
table

strings, numbers, booleans

{{#invoke:Test|query|Fox_Fennec|description}}
A small fox originally from the northern part of Earth's Africa continent. It hunts small creatures and has very large ears for cooling itself in the heat.
{{#invoke:Test|query|Fox_Fennec|MoveSpeed}}
4.6
{{#invoke:Test|query|Caribou|herdAnimal}}
true

"herdAnimal" is located in a branch of the main Def called "races" but because it appears nowhere else in the Def no additional parameters are needed to uniquely identify it. The following query retrieves the same data (just as an example) but with the use of an additional argument.

{{#invoke:Test|query|Caribou|race|herdAnimal}}
true

numbered lists (tradeTags)

{{#invoke:Test|query|GuineaPig|tradeTags}}
table

Query function returned "table" so its contents is displayed in the log (for reference). To get the length:

{{#invoke:Test|count|GuineaPig|tradeTags}}
3

Count also shows the table in the log. To get the second item:

{{#invoke:Test|query|GuineaPig|tradeTags|2}}
AnimalUncommon

sibling queries (lifeStageAges, tools, etc.)

{{#invoke:Test|query|GuineaPig|minAge|sibling=AnimalAdult}}
0.3

Same destination, different road:

{{#invoke:Test|query|GuineaPig|lifeStageAges|3|minAge}}
0.3

Some examples for "tools" (attacks):

{{#invoke:Test|count|Mech_Scyther|tools}}
3
{{#invoke:Test|count|Mech_Scyther|tools|1|capacities}}
2
{{#invoke:Test|query|Mech_Scyther|tools|1|capacities|1}}
Cut
{{#invoke:Test|query|Mech_Scyther|tools|1|capacities|2}}
Stab
{{#invoke:Test|query|Mech_Scyther|power|sibling=LeftBlade}}
20

Same result:

{{#invoke:Test|query|Mech_Scyther|power|sibling=left blade}}
20

Note for the two examples above (sibling=LeftBlade and sibling=left blade): these will retrieve the same data because:

    ["tools"] = {
      {
        ["label"] = "left blade",
        ["capacities"] = {
          "Cut",
          "Stab",
        },
        ["power"] = 20,
        ["cooldownTime"] = 2,
        ["linkedBodyPartsGroup"] = "LeftBlade",
        ["alwaysTreatAsWeapon"] = true,
      },

For the head they might be:

{{#invoke:Test|query|Mech_Scyther|power|sibling=HeadAttackTool}}
9
{{#invoke:Test|query|Mech_Scyther|power|sibling=head}}
9

This is the source data for it:

      {
        ["label"] = "head",
        ["capacities"] = {
          "Blunt",
        },
        ["power"] = 9,
        ["cooldownTime"] = 2,
        ["linkedBodyPartsGroup"] = "HeadAttackTool",
        ["chanceFactor"] = 0.2,
      },

getDefName

{{#invoke:Test|getDefName|fEnNeC foX}}
Fox_Fennec

local p = {}

------------------------------------------------------------------
-- deal with differences between MediaWiki and dev environments --
------------------------------------------------------------------

if mw then
  ENV = "wiki"
  log = mw.log

  util = require("Module:Test/lib/util")
  search = require("Module:Test/lib/search")
else
  ENV = "dev"

  mw = {}
  log = {}

  inspect = require './lib/inspect'
  util = require("./lib/util")
  search = require("./lib/search")
  diet = require("./data/diet")

  function pp(tbl, title) -- pretty print tables
    util.hl(title)
    print(inspect(tbl))
  end

  -- (re)define used mw functions that don't exist in dev environment
  mw.logObject = function(obj, prefix)
    if prefix then
      assert(type(prefix) == "string")
      table.insert(log, prefix .. " = " .. inspect(obj))
    else
      table.insert(log, inspect(obj))
    end
  end

  mw.dumpObject = function(arg)
    return inspect(arg)
  end

  mw.log = function(arg)
    table.insert(log, arg)
  end
end

---------------
-- load data --
---------------

if ENV == "dev" then
  data = loadfile("../output.lua")()
  diet = loadfile("./data/diet.lua")()
elseif ENV == "wiki" then
  data = mw.loadData('Module:Test/data')
end

version = data.version

------------------
-- virtual keys --
------------------

local virtual_store = {}
local virtual_keys = {
  ["Pawn"] = {

    function (def)
      virtField = "lives_in"
      biomes = {}
      for k,v in pairs(data) do
        prefix = string.match(k, '(.+):')
        if prefix == "BiomeDef" then
          table.insert(biomes, v)
        end
      end

      local list = {}
      for _,biome in pairs(biomes) do
        for animal,_ in pairs(biome.wildAnimals or {}) do
          if def.defName == animal then
            table.insert(list, biome.label)
          end
        end
      end

      def._virtual_[virtField] = list
    end,

    function (def)
      virtField = "foodTypes"
      foodTypes = def.race.foodType
      flags = {}
      virtual_store.diet = {}

      if util.diet.foodType[foodType] ~= "table" then
        return nil
      end

      for _,foodType in ipairs(foodTypes) do
        for foodItem,_ in pairs(util.diet.foodType[foodType]) do
          flags[foodItem] = true
        end
      end

      for flag,_ in pairs(flags) do
        table.insert(virtual_store.diet, flag)
      end

      def._virtual_[virtField] = virtual_store.diet
    end,

    function (def)
      virtField = "foodTypesExpanded"
      flags = {}
      eats = {}

      for _,def in pairs(data) do
        if def.defName and def.ingestible and def.ingestible.foodType then
            for _,ingestible in ipairs(def.ingestible.foodType) do
              for _,dietV in ipairs(virtual_store.diet) do
                if ingestible == dietV then
                  flags[def.defName] = true
                end
              end
            end
          end
        end

      for flag,_ in pairs(flags) do
        table.insert(eats, flag)
      end

      def._virtual_[virtField] = eats
    end,

  }
}

-----------------------
-- private functions --
-----------------------

local function vardefine(name, value)
  local f_name = "vardefine"
  assert(var_name, string.format("bad argument #1 to '%s' (argument missing, name of variable to define)", f_name))
  assert(var_name == "string", string.format("bad argument #1 to '%s' (string expected, got %s)", f_name, type(var_name)))
  assert(var_value, string.format("bad argument #2 to '%s' (argument missing, value to assign to variable)", f_name))
  assert(var_value == "string" or var_value == "number", string.format("bad argument #2 to '%s' (string or number expected, got %s)", f_name, type(var_value)))

  frame:callParserFunction('#vardefine', var_name, var_value)
end


local function mergeParents(baseDef, ignoreKeys)
  local ancestorIDs = {}
  local mergedDef = {}
  local def = baseDef

  while def._.ParentName do
    local parentID = def._.DefCategory .. ":" .. def._.ParentName
    table.insert(ancestorIDs, parentID)
    def = data[parentID]
  end

  ancestorIDs = util.table.reverse(ancestorIDs)
  table.insert(ancestorIDs, baseDef._.DefCategory .. ":" .. baseDef.defName)

  for _,parentID in ipairs(ancestorIDs) do
    util.table.overwrite(mergedDef, data[parentID], ignoreKeys)
  end

  return mergedDef
end


function getDef(defIDsuffix, defIDprefix)
  local ignoreKeys = {"Abstract", "Name", "ParentName"}
  local baseDef
  local mergedDef

  if defIDprefix then
    local defID = defIDprefix .. ":" .. defIDsuffix
    baseDef = data[defID]
    assert(not baseDef, string.format("getDef: Def '%s' not found", defID))
  else
    for defID,def in pairs(data) do
      -- WARNING: this depends on there not being any preexisting colons in the relevant substrings
      prefix = string.match(defID, '(.+):')
      suffix = string.match(defID, ':(.+)')
      if suffix == defIDsuffix then
        assert(not baseDef, string.format("getDef: Def conflict (more than one '%s')", defIDsuffix))
        baseDef = def
      end
    end
    assert(baseDef, string.format("getDef: Def '%s' not found", defIDsuffix))
  end

  mergedDef = mergeParents(baseDef, ignoreKeys)

  if virtual_keys[mergedDef.category] then
    mergedDef._virtual_ = {}
    for k,func in ipairs(virtual_keys[mergedDef.category]) do
      func(mergedDef)
    end
  end

  return mergedDef
end


----------------------
-- public interface --
----------------------

function p.getDefName(frame)
  local defName
  local label = frame.args[1]

  if not label then
    mw.logObject(frame.args, "frame.args")
    mw.log("getDefName: missing argument #1 (label)")
    return nil
  end

  for defID,def in pairs(data) do
    if string.upper(def.label or "") == string.upper(label) then
      defName = def.defName
    end
  end

  if not defName then
    mw.logObject(frame.args, "frame.args")
    mw.log(string.format("getDefName: '%s' not found", label))
  end

  return defName
end


function p.count(frame)
  local query = p.query(frame)
  return #query
end


function p.query(frame)
  local argLen = util.table.count(frame.args, "number") -- #frame.args won't work as expected, check the doc

  -- implement expressive argument checks so we know what's going on
  -- use them as a kind of usage guide (give as much info as possible)

  if not frame.args[1] then
    mw.logObject(frame.args, "frame.args")
    mw.log("query: missing argument #1 (defName or Name, for abstract Defs)")
    return nil
  end

  local def = getDef(frame.args[1])

  if not def then
    mw.logObject(frame.args, "frame.args")
    mw.log(string.format("query: bad argument #1 ('%s' not found)", frame.args[1]))
    return nil
  end

  local prune = def

  for i,arg in ipairs(frame.args) do -- arguments

    arg = tonumber(arg) or arg -- frame.args are always strings on MediaWiki so convert back the numbers

    -- NOTE: might consider doing something about the if tree (trim it down a bit)

    if i > 1 then -- additional arguments

      if i == argLen then -- if final argument

        if frame.args["sibling"] then -- sibling
          prune = search.conductor({nil, frame.args["sibling"]} , prune)
          if not prune then
            mw.logObject(frame.args, "frame.args")
            mw.log(string.format("query: bad argument 'sibling' ('%s' not found in '%s')", frame.args["sibling"], frame.args[i-1]))
            return nil
          else
            prune = prune.parent.table[arg]
            if not prune then
              mw.logObject(frame.args, "frame.args")
              mw.log(string.format("query: bad argument #%i ('%s' is not a sibling of '%s')", i, arg, frame.args["sibling"]))
            end
          end
        else
          prune = search.conductor(arg, prune)
          if not prune then
            mw.logObject(frame.args, "frame.args")
            mw.log(string.format("query: bad argument #%i ('%s' not found in '%s')", i, frame.args[i], frame.args[i-1]))
            return nil
          else
            prune = prune.value
          end
        end -- sibling

      else -- if not final argument
        prune = search.conductor(arg, prune)
        if not prune then
          mw.logObject(frame.args, "frame.args")
          mw.log(string.format("query: bad argument #%i ('%s' not found in '%s')", i, frame.args[i], frame.args[i-1]))
          return nil
        else
          prune = prune.value
        end
      end

    end -- additional arguments

  end -- for arguments

  if type(prune) == "table" then mw.logObject(prune) end

  return prune
end

---------------------------------
-- simulate module invocation  --
---------------------------------

local simframe = { ["args"] = {} }
frame = frame or simframe

--~ simframe.args[1] = "fennec fox"
simframe.args[1] = "Fox_Fennec"
simframe.args[2] = "MoveSpeed"
--~ simframe.args[3] = 3

if ENV == "dev" then

--~   p.query(frame)
  pp(p.query(frame))
--~   pp(p.getDefName(frame))

end

local clock = string.format("os.clock(): %i ms", os.clock() * 1000)
mw.log("--" .. string.rep("-", #clock) .. "--")
mw.log("- " .. clock .. " -")
mw.log("--" .. string.rep("-", #clock) .. "--")

----------------------------------------
-- simulate wiki log while developing --
----------------------------------------

if ENV == "dev" then
  util.hl("log")
  for _,v in ipairs(log) do
    print(v)
  end
end

return p -- return module