Module:Gear

From BTAWiki
Revision as of 05:57, 1 June 2023 by Fulmir (talk | contribs)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

Documentation for this module may be created at Module:Gear/doc

-- Module:Gear handles the myriad of gear types.

-- start with p, the package we expose
local p = {}

-- core is lua-only methods, not for use in the public intertace
local core = {}

p.core = core

local id_field = {
  field = 'Id',
  type = 'String'
}

--
-- imports
--
local getArgs = require('Module:Arguments').getArgs
local cargo = mw.ext.cargo

--
-- schema
--
-- schema defines the various database tables and their database schema.
-- 
-- tables defined in schema have 2 or 3 fields:
-- * table - the name of the table that this schema defines
-- * fields - an array of fields belonging to this table. each field contains:
--   * field - the name of the field
--   * type - the database type of the field.
-- * parent_table - Optional. A reference to the parent table. Used for
--                  sub-types like Weapon and EngineCore.
core.schema = {}

-- gear is the schema for the master Gear table. gear is the parent type for
-- every piece of gear that might be mounted on Mech.
core.schema.gear = {
  table = 'Gear',
  fields = {
    id_field,
    {
      field = 'Cost',
      -- type defines the type of the field
      type = 'Integer',
    },{
      field = 'Rarity',
      type = 'Integer',
    },{
      field = 'Purchasable',
      type = 'Boolean',
    },{
      field = 'Manufacturer',
      type = 'String',
    },{
      field = 'Model',
      type = 'String',
    },{
      field = 'UIName',
      type = 'String',
    },{
      field = 'Name',
      type = 'String',
    },{
      field = 'Details',
      -- Details use the Text type, which is unindexed and intended for
      -- longer-form data.
      type = 'Text',
    },{
      field = 'Icon',
      type = 'String',
    },{
      field = 'ComponentType',
      type = 'String',
    },{
      field = 'ComponentSubType',
      type = 'String',
    },{
      field = 'InventorySize',
      type = 'Integer',
    },{
      field = 'Tonnage',
      type = 'Float',
    },{
      -- TODO(rust dev): this might actually just be a string
      field = 'AllowedLocations',
      type = 'List (,) of String',
    },{
      -- TODO(rust dev): this also might actually just be a string.
      field = 'DisallowedLocations',
      type = 'List (,) of String',
    },{
      field = 'BattleValue',
      type = 'Integer',
    },{
      field = 'Bonuses',
      type = 'List (,) of String',
    },{
      field = 'CustomCategories',
      type = 'List (,) of String',
    },
  },
}

-- heatsinks defines the Heatsinks table. For weird game reasons, engine parts
-- count as heat sinks. This relationship is reflected in the database schema,
-- but does not strictly need to be. However, that ship has already sailed.
core.schema.heatsink = {
  table = 'Heatsinks',
  -- parent_table points to the gear schema, because heatsink is a subtype of
  -- gear.
  parent_table = core.schema.gear,
  fields = {
    {
      field = 'Dissipation',
      type = 'Integer',
    },
  },
}

-- cooling defines the Cooling table. Cooling parts are those that represent
-- the Mech's cooling kit, most commonly either single or double heatsinks, but
-- other heatsink types exist.
core.schema.cooling = {
  table = 'Cooling',
  -- Cooling is a subtype of Heatsink, and so its parent is the Heatsink table.
  parent_table = core.schema.heatsink,
  fields = {
    {
      -- HeatsinkDefID is the ID of the Heatsink that this Cooling kit uses.
      field = 'HeatsinkDefID',
      type = 'String',
    },
  },
}

-- EngineHeatBlocks represent the E-Cooling on an engine.
core.schema.engineheatblock = {
  table = 'EngineHeatBlocks',
  parent_table = core.schema.heatsink,
  fields = {
    {
      field = 'HeatsinkCount',
      type = 'Integer',
    },
  },
}

-- EngineShield is the kind of engine; Standard, XL, XXL, etc.
core.schema.engineshield = {
  table = 'EngineShields',
  parent_table = core.schema.heatsink,
  fields = {
    {
      field = 'ReservedSlots',
      type = 'Integer',
    },{
      field = 'EngineFactor',
      type = 'Float',
    },
  },
}

-- EngineCore is the actual engine rating.
core.schema.enginecore = {
  parent_table = core.schema.heatsink,
  table = 'EngineCore',
  fields = {
    {
      field = 'Rating',
      type = 'Integer',
    },
  },
}

-- UpgradeDef encompasses all miscellaneous mech parts. This includes gyros,
-- actuators, quirks, EW modules, and others. It currently has no fields that
-- Gear does not, but its existence allows us to filter only Upgrade parts,
-- and future-proofs us for changes later.
core.schema.upgrade = {
  table = 'UpgradeDef',
  parent_table = core.schema.gear,
  fields = {},
}

-- JumpJets are obviously all JumpJets, but things like Partial Wings also
-- count as JumpJets
core.schema.jumpjet = {
  table = 'JumpJets',
  parent_table = core.schema.gear,
  fields = {
    {
      field = 'MinTonnage',
      type = 'Integer',
    },{
      field = 'MaxTonnage',
      type = 'Integer',
    },{
      field = 'JumpCapacity',
      type = 'Float',
    },
  },
}

-- Weapon defines a weapon. Like everything else, it is a subclass of gear.
core.schema.weapon = {
  table = 'Weapons',
  parent_table = core.schema.gear,
  fields = {
    {
      field = 'Category',
      type = 'String',
    },{
      field = 'Type',
      type = 'String',
    },{
      field = 'WeaponSubType',
      type = 'String',
    },{
      field = 'MinRange',
      type = 'Integer',
    },{
      field = 'MaxRange',
      type = 'Integer',
    },{
      field = 'RangeSplit',
      type = 'List (,) of Integer',
    },{
      field = 'AmmoCategory',
      type = 'String',
    },{
      field = 'StartingAmmoCapacity',
      type = 'Integer',
    },{
      field = 'HeatGenerated',
      type = 'Integer',
    },{
      field = 'Damage',
      type = 'Integer',
    },{
      field = 'OverheatedDamageMultiplier',
      type = 'Float',
    },{
      field = 'EvasiveDamageMultiplier',
      type = 'Float',
    },{
      field = 'EvasivePipsIgnored',
      type = 'Integer',
    },{
      field = 'DamageVariance',
      type = 'Float',
    },{
      field = 'HeatDamage',
      type = 'Integer',
    },{
      field = 'AccuracyModifier',
      type = 'Float',
    },{
      field = 'CriticalChanceMultiplier',
      type = 'Float',
    },{
      field = 'AOECapable',
      type = 'Boolean',
    },{
      field = 'IndirectFireCapable',
      type = 'Boolean',
    },{
      field = 'RefireModifier',
      type = 'Integer',
    },{
      field = 'ShotsWhenFired',
      type = 'Integer',
    },{
      field = 'ProjectilesPerShot',
      type = 'Integer',
    },{
      field = 'AttackRecoil',
      type = 'Integer',
    },{
      field = 'Instability',
      type = 'Integer',
    },{
      field = 'WeaponEffectID',
      type = 'String',
    },{
      field = 'NoMelee',
      type = 'Boolean',
    },
  },
}

-- Ammunition encompasses AmmunitionBoxes. There are acutally two kinds of defs
-- in BattleTech for Ammunition, but for our purposes, we only need the
-- AmmunitionBox, which is the gear item. However, when parsing ammo types, we
-- also look at the Ammunition object, which defines details about the actual
-- shots, because this is where the Category is stored. That, however, is not
-- necessary for the wiki side of things.
core.schema.ammunition = {
  table = 'Ammunition',
  parent_table = core.schema.gear,
  fields = {
    {
      field = 'Capacity',
      type = 'Integer',
    },{
      -- Category is what kind of gun shoots this ammo.
      field = 'Category',
      type = 'String',
    },{
      field = 'PerUnitCost',
      type = 'Integer',
    },
  }
}

--
-- Cargo
--
-- Handles the connection between this module and the Cargo database.
core.cargo = {}

-- core.cargo.cargo_store stores the data for the current schema's fields, and
-- then recursively stores the data of the parent tables' fields.
function core.cargo.store(frame, schema, tpl_args)
  -- When we store a bit of gear to the database, we have to store it to two or
  -- more different tables. However, it is a limitation of Cargo that we can
  -- only attach or declare one table per template. To get around this
  -- limitation, we have an 'attach' template for every table type, and we
  -- include every table needed for a given schema when we store the data.
  frame:expandTemplate{title=string.format('Template:Gear/cargo/attach/%s', schema.table), args={}}

  local data = {}
  data._table = schema.table

  -- there may be extraneous arguments in the tpl_args, so we can't pass them
  -- verbatim to cargo. Instead, we iterate through the fields of the table,
  -- and we pull out the value of the parameter that matches the field.field.
  for _, field in ipairs(schema.fields) do
    local arg = tpl_args[field.field]
    if arg ~= nil then
      data[field.field] = arg
    end
  end

  -- this is where the magic happens. if the schema we're using is a sub-type
  -- of another schema, we call cargo_store recursively on the parent schema,
  -- to store all of the parent data as well.
  if schema.parent_table ~= nil then
    -- additionally, subtypes don't have the 'Id' field defined, so we add it
    -- manually here.
    data[id_field.field] = tpl_args[id_field.field]
    core.cargo.store(frame, schema.parent_table, tpl_args)
  end

  frame:callParserFunction('#cargo_store:', data)
end

-- core.cargo.cargo_declare declares a given table. Additionally, if the table is
-- has a parent table, we add an Id field to the declaration.
function core.cargo.declare(schema)
  return function(frame)
    local dcl_args = {}

    dcl_args._table = schema.table

    for _, field in ipairs(schema.fields) do
      dcl_args[field.field] = field.type
    end
    if schema.parent_table ~= nil then
      dcl_args[id_field.field] = id_field.type
    end
    frame:callParserFunction('#cargo_declare:', dcl_args)
  end
end

-- tables is a recursive method, used by core.cargo.query, that adds the
-- schema's table and all of its parent tables to the list of tables we query.
function core.cargo.tables(schema)
  local table = schema.table
  if schema.parent_table ~= nil then
    return table .. "," .. core.cargo.tables(schema.parent_table)
  else
    return table
  end
end

-- join_parents takes a schema and creates the join conditions for joining that
-- schema to its parent schema. it is a recursive method; after joining the
-- schema and it's parent, it calls join_parents again to add the join
-- conditions for the parent's parent as well, and so on.
function core.cargo.join_parents(schema)
  -- if the schema has no parent, then we have nothing to join to
  if schema.parent_table == nil then
    return nil
  end

  -- otherwise, if the schema does have a parent, then join to the parent
  local join = string.format(
    "%s.%s=%s.%s", 
    schema.table, id_field.field,
    schema.parent_table.table, id_field.field
  )

  -- now, call join_parents for the parent table. if the result is not nil,
  -- then we need to add the parent table's join to its parent.
  local parent_join = core.cargo.join_parents(schema.parent_table)
  if parent_join ~= nil then
    return join .. "," .. parent_join
  end

  return join
end

-- core.cargo.query is probably the most useful function in this library for
-- any outside user. It handles the details of adding all of the tables and
-- joining all of the parents of a schema. It takes 3 arguments:
--
-- * schema - the schema that we want to query, which should be one of
--            core.schema
-- * fields - the fields we want to query. this is one of the places that the
--            abstraction of core.cargo.query leaks through, as fields must be
--            specified in the form tablename.fieldname, so you must know the
--            table that the field you want belongs to.
-- * args   - the arguments to pass to the cargo query. If this contains a
--            join, it will be added to the end of the join_parents output.
--            Additionally, args takes a special argument, "extratables", a
--            string that specifies any extra tables that should be queried.
function core.cargo.query(schema, fields, args)
  if args == nil then
    args = {}
  end

  local tables = core.cargo.tables(schema)

  local join = core.cargo.join_parents(schema)
  if join ~= nil then
    if args.join ~= nil then
      args.join = join .. ',' .. args.join
    else
      args.join = join
    end
  end

  if args.extratables ~= nil and args.extratables ~= '' then
    tables = tables .. ',' .. args.extratables
  end

  return cargo.query(tables, fields, args)
end

--
-- Helpers
-- 
-- these functions are miscellaneous helper functions
function core.format_table(title, tpl_args)
  local rawTable = mw.html.create('table')
  rawTable:addClass('wikitable')

  rawTable:tag('tr'):tag('th'):attr('colspan', '2'):wikitext(title)

  for param, arg in pairs(tpl_args) do
    rawTable:tag('tr')
      :tag('th'):wikitext(param):done()
      :tag('td'):wikitext(arg)
  end

  return rawTable
end

--
-- core.get_gear retrieves all of the gear, fixed and otherwise, for the given
-- mech. It only returns data from the actual gear table, so any data from
-- subtypes needs to be queried separately.
--
-- returns an array of gear objects, each of which has these fields:
-- * location   - the MountedLocation of the gear
-- * name       - the Name of the gear
-- * uiname     - the UIName of the gear, which is usually a shorter name.
-- * id         - the Id of the gear
-- * fixed      - whether or not this is FixedEquipment
-- * categories - an array of strings of categories of the gear
function core.get_gear(chassisID, mechID)
  local args = {
    join = 'Gear.Id=MechInventory.ComponentDefID',
    where = string.format(
      'MechInventory.MechID="%s" OR MechInventory.MechID="%s"',
      chassisID, mechID
    ),
    extratables = 'MechInventory',
  }


  local gearData = core.cargo.query(core.schema.gear, 'Gear.Name,Gear.UIName,MechInventory.MountedLocation,MechInventory.FixedEquipment,Gear.Id,Gear.CustomCategories', args)
  local gear = {}
  for _, item in ipairs(gearData) do
    local fixedField = item['MechInventory.FixedEquipment']
    table.insert(gear, {
      location = item['MechInventory.MountedLocation'],
      name = item['Gear.Name'],
      uiname = item['Gear.UIName'],
      id = item['Gear.Id'],
      fixed = (fixedField == '1'),
      categories = mw.text.split(item['Gear.CustomCategories'], ','),
    })
  end

  return gear
end

-- core.get_engine returns an object representing details about the engine in
-- the given mech
-- 
-- returns an engine object, which has these fields:
-- * shield      - an object for the engine shield, having these fields:
--   * name      - the Name of the shield
--   * id        - the Id of the gear representing the shield
--   * fixed     - whether the engine shield is fixed.
-- * core        - an Integer representing the core rating of the enginecore
-- * tonnage     - the total tonnage of the engine core, sans shield modifiers
--                 and E-Cooling. not all that useful yet, needs to be updated
-- * ecooling    - the number of E-Cooling heatsinks.l
-- * cooling     - the Id of the Heatsink type that this engine uses.
-- * heatsinkkit - the Name of the Heatsink kit that this engine uses.
-- * dissipation - the total dissipation capacity of this engine, including
--                 free heat sinks and E-Cooling heat sinks, but _not_
--                 including mandatory external heat sinks that are required on
--                 smaller engines.
-- * fixed       - whether the engine core is fixed or replaceable.
--
-- These fields were added after the fact because of a mistake in API design:
-- * cooling_fixed  - whether the cooling system (heat sink kit) is fixed
-- * ecooling_fixed - whether the ecooling is fixed
function core.get_engine(chassisID, mechID)
  local engine = {}

  local where = string.format(
    'MechInventory.MechID="%s" OR MechInventory.MechID="%s"',
    chassisID, mechID
  )
  local join = 'Gear.Id=MechInventory.ComponentDefID'
  local extratables = 'MechInventory'

  local shieldRow = core.cargo.query(core.schema.engineshield, 'Gear.Name,Gear.Id,MechInventory.FixedEquipment', 
    { where = where, join = join, extratables = extratables }
  )
  if shieldRow[1] == nil then
    return nil
  end
  engine.shield = {
    name = shieldRow[1]['Gear.Name'],
    id = shieldRow[1]['Gear.Id'],
    fixed = shieldRow[1]['MechInventory.FixedEquipment'] == '1' 
  }

  local coreRow = core.cargo.query(
    core.schema.enginecore, 'EngineCore.Rating,Gear.Tonnage,MechInventory.FixedEquipment',
    { where = where, join = join, extratables = extratables }
  )
  engine.core = tonumber(coreRow[1]['EngineCore.Rating'], 10)
  engine.tonnage = tonumber(coreRow[1]['Gear.Tonnage'], 10)
  engine.fixed = (coreRow[1]['MechInventory.FixedEquipment'] == '1' or coreRow[1]['MechInventory.FixedEquipment'] == 'yes')

  local ecoolingRow = core.cargo.query(
    core.schema.engineheatblock, 'EngineHeatBlocks.HeatsinkCount,MechInventory.FixedEquipment',
    { where = where, join = join, extratables = extratables }
  )
  if ecoolingRow[1] == nil then
    engine.ecooling = 0
    engine.ecooling_fixed = false
  else
    engine.ecooling = tonumber(ecoolingRow[1]['EngineHeatBlocks.HeatsinkCount'], 10)
    engine.ecooling_fixed = ecoolingRow[1]['MechInventory.FixedEquipment'] == '1'
  end

  local coolingRow = core.cargo.query(
    core.schema.cooling, 'Cooling.HeatsinkDefID,Gear.Name,MechInventory.FixedEquipment',
    { where = where, join = join, extratables = extratables }
  )

  engine.cooling = coolingRow[1]['Cooling.HeatsinkDefID']
  engine.heatsinkkit = coolingRow[1]['Gear.Name']
  engine.cooling_fixed = coolingRow[1]['MechInventory.FixedEquipment'] == '1'

  local heatsinks = core.cargo.query(
    core.schema.heatsink, 'Heatsinks.Dissipation',
    { where = string.format('%s="%s"', 'Gear.Id', engine.cooling) }
  )
  local dissipation = tonumber(heatsinks[1]['Heatsinks.Dissipation'], 10)

  -- engines have internal heat sinks, which don't take up any space. smaller
  -- engines can mount fewer heat sinks, while larger engines can mount more.
  -- to compute the total effective heat sinks of the engine heat sinks, we
  -- divide the engine rating by 25 and take the floor value.
  -- so, for example, a 240-rated engine has 240/25 = 9.6, 9 heat sinks.
  --
  -- additionally, engines over 250 can mount additional heat sinks on the
  -- engine which add weight but take up no space. these are e-cooling.
  -- 300 / 25 = 12
  --
  -- to calculate engine heat sinks, we divide the engine rating by 25 and take
  -- the floor of that. then, we take the lesser of that value and 10, as 10
  -- is the maximum number of heat sinks that an engine can hold outside of
  -- ecooling
  --
  -- finally, we multiply the number of engine heatsinks by the dissipation
  -- per sink
  engine.heat_sinking = dissipation * (math.min(math.floor(engine.core / 25), 10) + engine.ecooling)

  return engine
end

-- core.get_heatsinks returns information about the external, non-engine
-- heatsinks of a mech.
--
-- returns an object with the follwoign fields:
-- * name        - the name of the heatsink
-- * dissipation - the dissipation capacity of the heatsink
function core.get_heatsinks(chassisID, mechID)
  local where  = string.format(
    'MechInventory.MechID="%s" OR MechInventory.MechID="%s"',
    chassisID, mechID
  )
  local join = 'Gear.Id=MechInventory.ComponentDefID'
  local fields = {
    'Gear.Name',
    'Heatsinks.Dissipation',
  }
  local extratables = 'MechInventory'

  local heatsinkRows = core.cargo.query(
    core.schema.heatsink,
    table.concat(fields, ','),
    { join = join, where = where, extratables = extratables }
  )

  local heatsinks = {}
  for _, heatsink in ipairs(heatsinkRows) do
      local dissipation = tonumber(heatsink['Heatsinks.Dissipation'], 10)
      if dissipation > 0 then
        table.insert(heatsinks, {
          name = heatsink['Gear.Name'],
          dissipation = dissipation,
        })
      end
  end

  return heatsinks
end

-- core.get_weapons returns information about all of the weapons on the given
-- mech.
-- 
-- returns an object with the following fields:
-- * name        - the name of the weapon
-- * category    - the category of the weapon
-- * type        - the type of the weapon
-- * ammo        - the AmmoCategory of the weapon.
-- * minRange    - the weapon's minimum range
-- * maxRange    - the weapon's maximum range
-- * heat        - the heat generated when firing the weapon
-- * damage      - the damage caused to the target by this weapon, with
--                 standard ammo if applicable.
-- * heatDamage  - the heat damage caused to the target by this weapon, with
--                 standard ammo if applicable.
-- * instability - the stability damage caused to the attacker by this weapon,
--                 with standard ammo if applicable.
function core.get_weapons(chassisID, mechID)
  local weapons = {}
  local where  = string.format(
    'MechInventory.MechID="%s" OR MechInventory.MechID="%s"',
    chassisID, mechID
  )
  local join = 'Gear.Id=MechInventory.ComponentDefID'

  local fields = {
    'Gear.Name',
    'Weapons.Category',
    'Weapons.Type',
    'Weapons.AmmoCategory',
    'Weapons.MinRange',
    'Weapons.MaxRange',
    'Weapons.HeatGenerated',
    'Weapons.Damage',
    'Weapons.HeatDamage',
    'Weapons.Instability',
    'Weapons.ShotsWhenFired',
  }

  local extratables = 'MechInventory'

  local weaponsResult = core.cargo.query(
    core.schema.weapon, table.concat(fields, ','), 
    {where = where, join = join, extratables = extratables}
  )

  for _, weaponRow in ipairs(weaponsResult) do
    local weapon = {
      name = weaponRow['Gear.Name'],
      category = weaponRow['Weapons.Category'],
      type = weaponRow['Weapons.Type'],
      ammo = weaponRow['Weapons.AmmoCategory'],
      minRange = tonumber(weaponRow['Weapons.MinRange'], 10),
      maxRange = tonumber(weaponRow['Weapons.MaxRange'], 10),
      heat = tonumber(weaponRow['Weapons.HeatGenerated'], 10),
      damage = tonumber(weaponRow['Weapons.Damage'], 10),
      shots = tonumber(weaponRow['Weapons.ShotsWhenFired'], 10),
      heatDamage = tonumber(weaponRow['Weapons.HeatDamage'], 10),
      instability = tonumber(weaponRow['Weapons.Instability'], 10),
    }
    
    table.insert(weapons, weapon)
  end

  return weapons
end

-- core.get_jumpjets returns a list of jumpjets on the mech
function core.get_jumpjets(chassisID, mechID)
  local jumpjets = {}
  local where  = string.format(
    'MechInventory.MechID="%s" OR MechInventory.MechID="%s"',
    chassisID, mechID
  )
  local join = 'Gear.Id=MechInventory.ComponentDefID'

  local fields = {
    'Gear.Id',
    'JumpJets.JumpCapacity',
    'MechInventory.MountedLocation',
  }

  local extratables = 'MechInventory'

  local jumpJetData = core.cargo.query(
    core.schema.jumpjet, table.concat(fields, ','),
    { where = where, join = join, extratables = extratables }
  )

  for _, jumpJetRow in ipairs(jumpJetData) do
    table.insert(jumpjets, {
      id = jumpJetRow['Gear.Id'],
      jump = tonumber(jumpJetRow['JumpJets.JumpCapacity']),
      location = jumpJetRow['MechInventory.MountedLocation']
    })
  end

  return jumpjets
end

--
-- Templates
--
-- these functions define templates.
--

-- all of these functions are invoked to declare the cargo tables for the
-- defined schemas.
p.table_gear = core.cargo.declare(core.schema.gear)
p.table_heatsink = core.cargo.declare(core.schema.heatsink)
p.table_cooling = core.cargo.declare(core.schema.cooling)
p.table_engineheatblock = core.cargo.declare(core.schema.engineheatblock)
p.table_engineshield = core.cargo.declare(core.schema.engineshield)
p.table_enginecore = core.cargo.declare(core.schema.enginecore)
p.table_upgrade = core.cargo.declare(core.schema.upgrade)
p.table_jumpjet = core.cargo.declare(core.schema.jumpjet)
p.table_weapon = core.cargo.declare(core.schema.weapon)
p.table_ammunition = core.cargo.declare(core.schema.ammunition)

--
-- All of the following define the template for defining a given piece of gear.
function p.heatsink(frame) 
  local tpl_args = getArgs(frame, {parentFirst = true})
  core.cargo.store(frame, core.schema.heatsink, tpl_args)

  return core.format_table('Heat Sink', tpl_args)
end

function p.cooling(frame)
  local tpl_args = getArgs(frame, {parentFirst = true})
  core.cargo.store(frame, core.schema.cooling, tpl_args)

  return core.format_table('Cooling', tpl_args)
end


function p.engineheatblock(frame)
  local tpl_args = getArgs(frame, {parentFirst = true})
  core.cargo.store(frame, core.schema.engineheatblock, tpl_args)

  return core.format_table('Engine Heat Block', tpl_args)
end

function p.engineshield(frame)
  local tpl_args = getArgs(frame, {parentFirst = true})
  core.cargo.store(frame, core.schema.engineshield, tpl_args)

  return core.format_table('Engine Shield', tpl_args)
end

function p.enginecore(frame)
  local tpl_args = getArgs(frame, {parentFirst = true})
  core.cargo.store(frame, core.schema.enginecore, tpl_args)

  return core.format_table('Engine Core', tpl_args)
end

function p.upgrade(frame)
  local tpl_args = getArgs(frame, {parentFirst = true})

  core.cargo.store(frame, core.schema.upgrade, tpl_args)
  return core.format_table('Upgrade', tpl_args)
end

function p.jumpjet(frame)
  local tpl_args = getArgs(frame, {parentFirst = true})

  core.cargo.store(frame, core.schema.jumpjet, tpl_args)
  return core.format_table('Jump Jet', tpl_args)
end

function p.weapon(frame)
  local tpl_args = getArgs(frame, {parentFirst = true})

  core.cargo.store(frame, core.schema.weapon, tpl_args)
  return core.format_table('Weapon', tpl_args)
end

function p.ammunition(frame)
  local tpl_args = getArgs(frame, {parentFirst = true})

  core.cargo.store(frame, core.schema.ammunition, tpl_args)
  return core.format_table('Ammunition', tpl_args)
end

function p.weapons_list(frame)
  local tpl_args = getArgs(frame, {parentFirst = true})

  local simple_columns = {
    'UIName',
    'AmmoCategory',
    'Tonnage',
    'InventorySize',
    'Damage',
    'HeatDamage',
    'Instability',
    'ShotsWhenFired',
    'ProjectilesPerShot',
    'HeatGenerated',
    'AttackRecoil',
    'EvasivePipsIgnored',
    'CriticalChanceMultiplier',
  }
  
  local complex_columns = {
    'MinRange',
    'RangeSplit',
    'MaxRange',
  }

  local fields = table.concat(simple_columns, ',') .. ',' .. table.concat(complex_columns)

  local args = {}
  if tpl_args['Category'] ~= nil then
    args.where = string.format('Category="%s"',tpl_args['Category'])
  end

  local items = core.cargo.query(core.schema.weapon, fields, args)

  local t = mw.html.create('table')
  t:addClass('wikitable')

  local headrow = t:tag('tr')
  headrow:tag('th'):attr('colspan', '2')
  headrow:tag('th'):attr('colspan', '2'):wikitext('Size')
  headrow:tag('th'):attr('colspan', '3'):wikitext('Damage')
  headrow:tag('th'):attr('colspan', '4'):wikitext('Per Salvo')
  headrow:tag('th'):attr('colspan', '3'):wikitext('Modifiers')
  headrow:tag('th'):attr('colspan', '5'):wikitext('Range')

  local subhead = t:tag('tr')
  subhead:tag('th'):wikitext('Name')
  subhead:tag('th'):wikitext('Ammo')
  subhead:tag('th'):wikitext('Tonnage')
  subhead:tag('th'):wikitext('Slots')
  subhead:tag('th'):wikitext('Normal')
  subhead:tag('th'):wikitext('Heat')
  subhead:tag('th'):wikitext('Stab')
  subhead:tag('th'):wikitext('Shots')
  subhead:tag('th'):wikitext('Projectiles')
  subhead:tag('th'):wikitext('Heat')
  subhead:tag('th'):wikitext('Recoil')
  subhead:tag('th'):wikitext('Accuracy')
  subhead:tag('th'):wikitext('Evasion Ignored')
  subhead:tag('th'):wikitext('Bonus Crit Chance')
  subhead:tag('th'):wikitext('Min')
  subhead:tag('th'):wikitext('Short')
  subhead:tag('th'):wikitext('Medium')
  subhead:tag('th'):wikitext('Long')

  for _, item in ipairs(items) do
    local row = t:tag('tr')
    for _, column in ipairs(columns) do
      row:tag('td'):wikitext(item[column])
    end

    row:tag('td'):wikitext(item['MinRange'])
    local range_brackets = mw.text.split(item['RangeSplit'], ',')
    row:tag('td'):wikitext(range_brackets[1])
    row:tag('td'):wikitext(range_brackets[2])
    row:tag('td'):wikitext(range_brackets[3])
    row:tag('td'):wikitext(item['MaxRange'])
  end
end

-- get returns the fields for the given 
-- first arg is the type, the table name
-- second arg is the id of the object
-- any subsequent args are the fields to return
function p.get(frame)
  tpl_args = getArgs(frame, {parentFirst=true})
  -- the first named
  local table = tpl_args[1]
  local id = tpl_args[2]
  local fields = table.concat(tpl_args, ',', 3)

  local target_schema

  for _, schema in pairs(core.schema) do
    if schema.table == table then
      target = schema
    end
  end

  local row = core.cargo.query(schema, fields) 
end

-- always return p
return p