Die Dokumentation für dieses Modul kann unter Modul:VehicleHardpoint/Doku erstellt werden
require( 'strict' ) local VehicleHardpoint = {} local metatable = {} local methodtable = {} metatable.__index = methodtable local TNT = require( 'Module:Translate' ):new() local common = require( 'Module:Common' ) -- formatNum and spairs local hatnote = require( 'Module:Hatnote' )._hatnote local data = mw.loadJsonData( 'Module:VehicleHardpoint/data.json' ) local config = mw.loadJsonData( 'Module:VehicleHardpoint/config.json' ) --- Calls TNT with the given key --- --- @param key string The translation key --- @param addSuffix boolean Adds a language suffix if config.smw_multilingual_text is true --- @return string If the key was not found in the .tab page, the key is returned local function translate( key, addSuffix, ... ) return TNT:translate( 'Module:VehicleHardpoint/i18n.json', config, key, addSuffix, {...} ) or key end --- Checks if an entry contains a 'child' key with further entries --- --- @return boolean local function hasChildren( row ) return row.children ~= nil and type( row.children ) == 'table' and #row.children > 0 end --- Creates the object that is used to query the SMW store --- --- @param page string the vehicle page containing data --- @return table local function makeSmwQueryObject( page ) local langSuffix = '' if config.smw_multilingual_text == true then langSuffix = '+lang=' .. ( config.module_lang or mw.getContentLanguage():getCode() ) end return { string.format( '[[-Has subobject::' .. page .. ']][[%s::+]][[%s::+]]', translate( 'SMW_HardpointType' ), translate( 'SMW_VehicleHardpointsTemplateGroup' ) ), string.format( '?%s#-=from_gamedata', translate( 'SMW_FromGameData' ) ), string.format( '?%s#-=count', translate( 'SMW_ItemQuantity' ) ), string.format( '?%s#-=min_size', translate( 'SMW_HardpointMinimumSize' ) ), string.format( '?%s#-=max_size', translate( 'SMW_HardpointMaximumSize' ) ), string.format( '?%s#-=class', translate( 'SMW_VehicleHardpointsTemplateGroup' ) ), langSuffix, string.format( '?%s#-=type', translate( 'SMW_HardpointType' ) ), langSuffix, string.format( '?%s#-=sub_type', translate( 'SMW_HardpointSubtype' ) ), langSuffix, string.format( '?%s#-=name', translate( 'SMW_Name' ) ), string.format( '?%s#-n=scu', translate( 'SMW_Inventory' ) ), string.format( '?UUID#-=uuid' ), string.format( '?%s#-=hardpoint', translate( 'SMW_Hardpoint' ) ) , string.format( '?%s#-=class_name', translate( 'SMW_HardpointClassName' ) ) , string.format( '?%s#-=magazine_capacity', translate( 'SMW_MagazineCapacity' ) ), string.format( '?%s=thrust_capacity', translate( 'SMW_ThrustCapacity' ) ), string.format( '?%s=damage', translate( 'SMW_Damage' ) ), string.format( '?%s=damage_radius', translate( 'SMW_DamageRadius' ) ), string.format( '?%s=fuel_capacity', translate( 'SMW_FuelCapacity' ) ), string.format( '?%s=fuel_intake_rate', translate( 'SMW_FuelIntakeRate' ) ), string.format( '?%s#-=parent_hardpoint', translate( 'SMW_ParentHardpoint' ) ), string.format( '?%s#-=root_hardpoint', translate( 'SMW_RootHardpoint' ) ), string.format( '?%s#-=parent_uuid', translate( 'SMW_ParentHardpointUuid' ) ), string.format( '?%s#-=icon', translate( 'SMW_Icon' ) ), string.format( '?%s=hp', translate( 'SMW_HitPoints' ) ), string.format( '?%s#-=position', translate( 'SMW_Position' ) ), -- These are subquery chains, they require that the 'Name' attribute is of type Page -- And that these pages contain SMW attributes '?' .. translate( 'SMW_Name' ) .. '.' .. translate( 'SMW_Grade' ) .. '#-=item_grade', '?' .. translate( 'SMW_Name' ) .. '.' .. translate( 'SMW_Class' ) .. '#-=item_class', '?' .. translate( 'SMW_Name' ) .. '.' .. translate( 'SMW_Size' ) .. '#-=item_size', '?' .. translate( 'SMW_Name' ) .. '.' .. translate( 'SMW_Manufacturer' ) .. '#-=manufacturer', string.format( 'sort=%s,%s,%s,%s,%s', translate( 'SMW_VehicleHardpointsTemplateGroup' ), translate( 'SMW_Hardpoint' ), translate( 'SMW_HardpointType' ), translate( 'SMW_HardpointMaximumSize' ), translate( 'SMW_ItemQuantity' ) ), 'order=asc,desc,asc,asc,asc', 'limit=1000' } end --- Creates a 'key' based on various data points found on the hardpoint and item --- Based on this key, the count of some entries is generated --- --- @param row table - API Data --- @param hardpointData table - Data from getHardpointData --- @param parent table|nil - Parent hardpoint (A settable SMW Subobject) --- @param root string|nil - Root hardpoint --- @return string Key local function makeKey( row, hardpointData, parent, root ) local key -- If the hardpoint has an item attached if type( row.item ) == 'table' then -- List of item types that should always be grouped together -- i.e. their count is increased instead of them being displayed as separate boxes if row.type == 'ManneuverThruster' or row.type == 'MainThruster' or row.type == 'ArmorLocker' or row.type == 'Bed' or row.type == 'CargoGrid' or row.type == 'Cargo' then key = row.type .. row.sub_type else local suffix = ( row.item.name or '' ) if suffix == '<= PLACEHOLDER =>' then suffix = row.item.uuid end -- Adding the uuid to the key ensures separate boxes if the equipped item differs key = row.type .. row.sub_type .. suffix end else -- If no item is set, use the pre-defined class and type key = hardpointData.class .. hardpointData.type end -- Appends the parent and root hardpoints in order to not mess up child counts -- Without this, a vehicle with four turrets containing each one weapon would be listed as -- having four turrets that each has four weapons (if the exact weapon is equipped on each turret) if parent ~= nil and parent[ translate( 'SMW_Name' ) ] ~= nil and row.type ~= 'DecoyLauncherMagazine' and row.type ~= 'NoiseLauncherMagazine' then --key = key .. parent[ translate( 'SMW_Hardpoint' ) ] key = key .. ( parent[ translate( 'SMW_Name' ) ] or parent[ translate( 'SMW_Hardpoint' ) ] ) end if root ~= nil and not string.match( key, root ) and ( hardpointData.class == 'Weapons' or hardpointData.class == 'Utility' ) then key = key .. root end if hardpointData.class == 'Weapons' and row.name ~= nil and row.type == 'MissileLauncher' then -- key = key .. row.item.name or row.name end mw.logObject( string.format( 'Key: %s', key ), 'makeKey' ) return key end --- Get pre-defined hardpoint data for a given hardpoint type --- If no type is found, the hardpoint name is matched against the defined regexes until the first one matches --- --- @param hardpointType string --- @return table|nil function methodtable.getHardpointData( self, hardpointType ) if type( data.matches[ hardpointType ] ) == 'table' then return data.matches[ hardpointType ] end for hType, mappingData in pairs( data.matches ) do if hardpointType == hType then return mappingData elseif type( mappingData.matches ) == 'table' then for _, matcher in pairs( mappingData.matches ) do if string.match( hardpointType, matcher ) ~= nil then return mappingData end end end end return nil end --- Creates a child object for weapons and counter measure ammunitions --- As well as weapon ports on armor locker --- --- @param hardpoint table A hardpoint object form the API --- @return void local function addSubComponents( hardpoint ) if type( hardpoint.item ) ~= 'table' then return end if type( hardpoint.children ) ~= 'table' then hardpoint.children = {} end if hardpoint.item.type == 'WeaponDefensive' or hardpoint.item.type == 'WeaponGun' then local item_type = 'Magazine' if mw.ustring.sub( hardpoint.class_name, -5 ) == 'chaff' then item_type = 'NoiseLauncherMagazine' elseif mw.ustring.sub( hardpoint.class_name, -5 ) == 'flare' then item_type = 'DecoyLauncherMagazine' end local capacity = {} local magazineName = translate( 'Magazine' ) if hardpoint.item.type == 'WeaponGun' and type( hardpoint.item.vehicle_weapon ) == 'table' then table.insert( capacity, hardpoint.item.vehicle_weapon.capacity ) -- This is a laser weapon, add another capacity of -1 to indicate that this weapon has infinite ammo if type( hardpoint.item.vehicle_weapon.regeneration ) == 'table' then table.insert( capacity, -1 ) magazineName = translate( 'Capacitor' ) end elseif type( hardpoint.item.counter_measure ) == 'table' then table.insert( capacity, hardpoint.item.counter_measure.capacity ) end table.insert( hardpoint.children, { name = 'faux_hardpoint_magazine', class_name = 'FAUX_' .. item_type .. 'Magazine', type = item_type, sub_type = item_type, min_size = 1, max_size = 1, item = { name = magazineName, type = item_type, sub_type = item_type, magazine_capacity = capacity } } ) end -- This seems to be a weapon rack if ( hardpoint.item.type == 'Usable' or hardpoint.item.type == 'Door' ) and type( hardpoint.item.ports ) == 'table' then local item_type = 'WeaponPort' for _, port in pairs( hardpoint.item.ports ) do -- Prevent stuff like mattress and pillow to count as weapon ports (I don't think SC let you hide weapons inside them :P) if ( mw.ustring.find( port.name, 'weapon', 1, true ) or mw.ustring.find( port.display_name, 'weapon', 1, true ) ) then local sub_type = item_type .. tostring( port.sizes.min or 0 ) .. tostring( port.sizes.max or 0 ) local name = 'WeaponPort' if port.sizes.max == 5 or mw.ustring.find( port.display_name, 'launcher', 1, true ) then name = name .. 'Launcher' elseif port.sizes.max == 4 or mw.ustring.find( port.display_name, 'rifle', 1, true ) then name = name .. 'Rifle' elseif mw.ustring.find( port.display_name, 'multitool', 1, true ) then name = name .. 'Multitool' elseif mw.ustring.find( port.display_name, 'addon', 1, true ) then name = name .. 'Addon' -- Assume size 1 is pistol slot if it is not specified as multitool or addon elseif port.sizes.max == 1 or mw.ustring.find( port.display_name, 'pistol', 1, true ) then name = name .. 'Pistol' end table.insert( hardpoint.children, { name = 'faux_hardpoint_weaponport', class_name = 'FAUX_WeaponPort', type = item_type, sub_type = sub_type, min_size = port.sizes.min, max_size = port.sizes.max, item = { name = translate( name ), type = item_type, sub_type = sub_type, } } ) end end end end --- Builds the object that is saved to SMW as a Subobject --- --- @param row table - API Data --- @param hardpointData table - Data from getHardpointData --- @param parent table|nil - Parent hardpoint --- @param root string|nil - Root hardpoint --- @return table function methodtable.makeObject( self, row, hardpointData, parent, root ) local object = {} if hardpointData == nil then hardpointData = self:getHardpointData( row.type or row.name ) end if hardpointData == nil then return nil end object[ translate( 'SMW_Hardpoint' ) ] = row.name object[ translate( 'SMW_FromGameData' ) ] = true object[ translate( 'SMW_HardpointMinimumSize' ) ] = row.min_size object[ translate( 'SMW_HardpointMaximumSize' ) ] = row.max_size object[ translate( 'SMW_VehicleHardpointsTemplateGroup' ) ] = translate( hardpointData.class, true ) object[ translate( 'SMW_HitPoints' ) ] = row.damage_max object[ translate( 'SMW_Position' ) ] = row.position if type( row.class_name ) == 'string' then object[ translate( 'SMW_HardpointClassName' ) ] = row.class_name end object[ translate( 'SMW_HardpointType' ) ] = translate( hardpointData.type, true ) object[ translate( 'SMW_HardpointSubtype' ) ] = translate( hardpointData.type, true ) -- FIXME: Is there a way to use Lua table key directly instead of setting subtype separately in data.json? -- For some components (e.g. missile), the key is the subtype of the component local function setTypeSubtype( match ) if match ~= nil then if match.type ~= nil then object[ translate( 'SMW_HardpointType' ) ] = translate( match.type, true ) end if match.subtype ~= nil then object[ translate( 'SMW_HardpointSubtype' ) ] = translate( match.subtype, true ) end end end setTypeSubtype( data.matches[ row.type ] ) setTypeSubtype( data.matches[ row.sub_type ] ) if hardpointData.item ~= nil and type( hardpointData.item.name ) == 'string' then object[ translate( 'SMW_Name' ) ] = hardpointData.item.name end if type( row.item ) == 'table' then local itemObj = row.item if itemObj.name ~= '<= PLACEHOLDER =>' then local match = string.match( row.class_name or '', '[Dd]estruct_(%d+s)' ) if row.type == 'SelfDestruct' and match ~= nil then object[ translate( 'SMW_Name' ) ] = string.format( '%s (%s)', translate( 'SMW_SelfDestruct' ), match ) -- Set self-destruct stats -- FIXME: Do subquery instead when CIG properly implement self-destruct components if itemObj.self_destruct then object[ translate( 'SMW_Damage' ) ] = itemObj.self_destruct.damage object[ translate( 'SMW_DamageRadius' ) ] = itemObj.self_destruct.radius end else object[ translate( 'SMW_Name' ) ] = itemObj.name end else object[ translate( 'SMW_Name' ) ] = object[ translate( 'SMW_HardpointSubtype' ) ] -- Remove lang suffix local parts = mw.text.split( object[ translate( 'SMW_Name' ) ], '@', true ) object[ translate( 'SMW_Name' ) ] = parts[ 1 ] or object[ translate( 'SMW_Name' ) ] end object[ translate( 'SMW_MagazineCapacity' ) ] = itemObj.magazine_capacity if ( itemObj.type == 'Cargo' or itemObj.type == 'SeatAccess' or itemObj.type == 'CargoGrid' or itemObj.type == 'Container' ) and type( itemObj.inventory ) == 'table' then object[ translate( 'SMW_Inventory' ) ] = common.formatNum( (itemObj.inventory.scu or nil ), nil ) end if itemObj.thruster then object[ translate( 'SMW_ThrustCapacity' ) ] = itemObj.thruster.thrust_capacity --- Convert to per Newton since thrust capacity is in Newton object[ translate( 'SMW_FuelBurnRate' ) ] = itemObj.thruster.fuel_burn_per_10k_newton / 10000 end if itemObj.fuel_tank and itemObj.fuel_tank.capacity > 0 then object[ translate( 'SMW_FuelCapacity' ) ] = itemObj.fuel_tank.capacity end if itemObj.fuel_intake then object[ translate( 'SMW_FuelIntakeRate' ) ] = itemObj.fuel_intake.fuel_push_rate end if object[ translate( 'SMW_HardpointMinimumSize' ) ] == nil then object[ translate( 'SMW_HardpointMinimumSize' ) ] = itemObj.size object[ translate( 'SMW_HardpointMaximumSize' ) ] = itemObj.size end object[ 'UUID' ] = row.item.uuid end if parent ~= nil then object[ translate( 'SMW_ParentHardpointUuid' ) ] = parent[ 'UUID' ] object[ translate( 'SMW_ParentHardpoint' ) ] = parent[ translate( 'SMW_Name' ) ] end if root ~= nil then object[ translate( 'SMW_RootHardpoint' ) ] = root end -- Icon local icon = hardpointData.type if data.section_label_fixes[ hardpointData.class ] ~= nil or data.section_label_fixes[ hardpointData.type ] ~= nil then icon = data.section_label_fixes[ hardpointData.class ] or data.section_label_fixes[ hardpointData.type ] end for hType, iconKey in pairs( data.icons ) do if hType == icon then -- Disable label missing icons for now if iconKey == '' then icon = nil break end -- Apply icon key override icon = iconKey end end if icon ~= nil then if config.icon_name_localized == true then icon = translate( icon ) end if config.icon_name_lowercase == true then icon = string.lower( icon ) end object[ translate( 'SMW_Icon' ) ] = string.format( 'File:%s%s.svg', config.icon_prefix, icon ) end -- Remove SeatAccess Hardpoints without storage if row.item ~= nil and row.item.type == 'SeatAccess' and object[ translate( 'SMW_Inventory' ) ] == nil then object = nil end return object; end --- Sets all available hardpoints as SMW subobjects --- This method should be called by the accompanying Vehicle Module --- --- @param hardpoints table API Hardpoint data function methodtable.setHardPointObjects( self, hardpoints ) if type( hardpoints ) ~= 'table' then error( translate( 'msg_invalid_hardpoints_object' ) ) end local objects = {} local depth = 1 local function cleanClassName( input ) if string.find( input, 'turret', 1, true ) then local parts = mw.text.split( input, 'turret', true ) input = parts[ 1 ] or input end for _, remove in pairs( { 'top', 'bottom', 'left', 'right', 'front', 'rear', 'bubble', 'side' } ) do input = string.gsub( input, '_' .. remove, '', 1 ) end return input end -- Adds the subobject to the list of objects that should be saved to SMW -- Increases the item quantity / or combined cargo capacity for objects that have equal keys local function addToOut( object, key ) if object == nil then return end -- If this key (object) has not been seen before, save it to the list of subobjects if type( objects[ key ] ) ~= 'table' then if object ~= nil then objects[ key ] = object objects[ key ][ translate( 'SMW_ItemQuantity' ) ] = 1 end else -- This key (object) has been seen before: Increase the quantity and any other cumulative metrics objects[ key ][ translate( 'SMW_ItemQuantity' ) ] = objects[ key ][ translate( 'SMW_ItemQuantity' ) ] + 1 if object[ translate( 'SMW_Position' ) ] ~= nil then if type( objects[ key ][ translate( 'SMW_Position' ) ] ) == 'table' then table.insert( objects[ key ][ translate( 'SMW_Position' ) ], object[ translate( 'SMW_Position' ) ] ) else objects[ key ][ translate( 'SMW_Position' ) ] = { objects[ key ][ translate( 'SMW_Position' ) ], object[ translate( 'SMW_Position' ) ] } end end local inventoryKey = translate( 'SMW_Inventory' ) -- Accumulate the cargo capacities of all cargo grids if object[ inventoryKey ] ~= nil then objects[ key ][ translate( 'SMW_ItemQuantity' ) ] = 1 if objects[ key ][ inventoryKey ] ~= nil and object[ inventoryKey ] ~= nil then local sucExisting, numExisting = pcall( tonumber, objects[ key ][ inventoryKey ], 10 ) local sucNew, numNew = pcall( tonumber, object[ inventoryKey ], 10 ) if sucExisting and sucNew and numExisting ~= nil and numNew ~= nil then objects[ key ][ inventoryKey ] = numExisting + numNew end end end end end -- Iterates through the list of hardpoints found on the API object local function addHardpoints( hardpoints, parent, root ) for _, hardpoint in pairs( hardpoints ) do hardpoint.name = string.lower( hardpoint.name ) if type( hardpoint.class_name ) == 'string' then hardpoint.class_name = cleanClassName( string.lower( hardpoint.class_name ) ) end hardpoint = VehicleHardpoint.fixTypes( hardpoint, data.fixes ) local hardpointData = self:getHardpointData( hardpoint.type or hardpoint.name ) if hardpointData ~= nil then if depth == 1 then if type( hardpoint.item ) == 'table' then root = hardpoint.class_name or hardpoint.name if root == '<= PLACEHOLDER =>' then root = hardpointData.type end else root = hardpoint.name end mw.logObject( string.format( 'Root: %s', root ), 'addHardpoints' ) end addSubComponents( hardpoint ) -- Based on the key, the hardpoint is either used as "standalone" (i.e. saved as a single subobject) -- or, if the key already exists, the count if increased by one (so no extra subobject is generated) local key = makeKey( hardpoint, hardpointData, parent, root ) local obj = self:makeObject( hardpoint, hardpointData, parent, root ) addToOut( obj, key ) -- Generate child subobjects if hasChildren( hardpoint ) then depth = depth + 1 addHardpoints( hardpoint.children, obj, root ) end end end depth = depth - 1 if depth < 1 then depth = 1 root = nil end end addHardpoints( hardpoints ) mw.logObject( objects, 'setHardPointObjects' ) for _, subobject in pairs( objects ) do mw.smw.subobject( subobject ) end end --- Sets all available vehicle parts as SMW subobjects --- This method should be called by the accompanying Vehicle Module --- --- @param parts table API Parts data function methodtable.setParts( self, parts ) if type( parts ) ~= 'table' then error( translate( 'msg_invalid_hardpoints_object' ) ) end local objects = {} local depth = 1 local partData = { class = 'VehiclePart', type = 'VehiclePart', } local function makeKey( row, parent ) local key = row.name if parent ~= nil then key = key .. parent[ translate( 'SMW_Hardpoint' ) ] end mw.logObject( string.format( 'Key: %s', key ), 'makeKey' ) return key end -- Adds the subobject to the list of objects that should be saved to SMW local function addToOut( object, key ) if object == nil then return end -- If this key (object) has not been seen before, save it to the list of subobjects if type( objects[ key ] ) ~= 'table' then if object ~= nil then objects[ key ] = object objects[ key ][ translate( 'SMW_ItemQuantity' ) ] = 1 end end end -- Iterates through the list of parts found on the API object local function addParts( parts, parent, root ) for _, part in pairs( parts ) do part.type = 'VehiclePart' part.min_size = 1 part.max_size = 1 part.item = { name = part.display_name } if depth == 1 then root = part.name mw.logObject( string.format( 'Root: %s', root ), 'addParts' ) end local key = makeKey( part, parent ) local obj = self:makeObject( part, partData, parent, root ) addToOut( obj, key ) -- Generate child subobjects if hasChildren( part ) then depth = depth + 1 addParts( part.children, obj, root ) end end depth = depth - 1 if depth < 1 then depth = 1 root = nil end end addParts( parts ) mw.logObject( objects, 'setParts' ) for _, subobject in pairs( objects ) do mw.smw.subobject( subobject ) end end --- Sets all available ship-matrix components as SMW subobjects --- This method should be called by the accompanying Vehicle Module --- --- @param components table API components data function methodtable.setComponents( self, components ) if type( components ) ~= 'table' then error( translate( 'msg_invalid_hardpoints_object' ) ) end local lang = mw.getContentLanguage() for _, component in pairs( components ) do local parts = mw.text.split( components.type, '_', true ) local type = '' for _, part in ipairs( parts ) do type = type .. lang:ucfirst( part ) end type = mw.text.trim( type, 's' ) mw.smw.subobject( { [ translate( 'SMW_VehicleHardpointsTemplateGroup' ) ] = translate( component.class, true ), [ translate( 'SMW_HardpointType' ) ] = translate( type, true ), [ translate( 'SMW_Name' ) ] = translate( component.name:gsub( ' ', '' ) ), [ translate( 'SMW_ItemQuantity' ) ] = component.quantity, --[ 'Komponentenbefestigungen' ] = component.mounts, [ translate( 'SMW_Size' ) ] = component.component_size, [ translate( 'SMW_HardpointMaximumSize' ) ] = component.size, [ translate( 'SMW_FromGameData' ) ] = false, } ) end mw.logObject( objects, 'setParts' ) for _, subobject in pairs( objects ) do mw.smw.subobject( subobject ) end end --- Queries the SMW store for all available hardpoint subobjects for a given page --- --- @param page string - The page to query --- @return table hardpoints function methodtable.querySmwStore( self, page ) -- Cache multiple calls if self.smwData ~= nil then return self.smwData end local smwData = mw.smw.ask( makeSmwQueryObject( page ) ) if smwData == nil or smwData[ 1 ] == nil then return nil end --mw.logObject( smwData, 'querySmwStore' ) self.smwData = smwData return self.smwData end --- Group Hardpoints by Class and type --- --- @param smwData table SMW data - Requires a 'class' key on each row --- @return table function methodtable.group( self, smwData ) local grouped = {} if type( smwData ) ~= 'table' then return {} end for _, row in ipairs( smwData ) do if not row.isChild and row.class ~= nil and row.type ~= nil and -- Specifically hide manually added weapon ports that have no parent -- This should not be needed anymore if weapon lockers are found everywhere with an uuid row.type ~= translate( 'WeaponPort' ) and not mw.ustring.find( row.type, translate( 'Magazine' ), 1, true ) then if type( grouped[ row.class ] ) ~= 'table' then grouped[ row.class ] = {} end if type( grouped[ row.class ][ row.type ] ) ~= 'table' then grouped[ row.class ][ row.type ] = {} end table.insert( grouped[ row.class ][ row.type ], row ) self.iconMap[ row.class ] = row.icon self.iconMap[ row.type ] = row.icon end end --mw.logObject( grouped ) return grouped end --- Adds children to the according parents --- --- @param smwData table All available Hardpoint objects for this page --- @return table The stratified table function methodtable.createDataStructure( self, smwData ) -- Maps a key to the index of the subobject, this way children can be set on their parent local idMapping = {} for index, object in ipairs( smwData ) do local keyMap if object.class == translate( 'VehiclePart' ) and object.name ~= nil then keyMap = object.name else keyMap = ( object.root_hardpoint or object.class_name or '' ) .. ( object.name or object.type or '' ) end idMapping[ keyMap ] = index end -- Iterates through the list of SMW hardpoint subobjects -- If the 'parent_hardpoint' key is set (i.e. the hardpoint is a child), it is added as a child to the parent object local function stratify( toStratify ) for _, object in ipairs( toStratify ) do if object.parent_hardpoint ~= nil then local parentEl if object.class == translate( 'VehiclePart' ) and object.parent_hardpoint ~= nil then parentEl = toStratify[ idMapping[ object.parent_hardpoint ] ] else parentEl = toStratify[ idMapping[ ( ( object.root_hardpoint or '' ) .. object.parent_hardpoint ) ] ] end if parentEl ~= nil then if parentEl.children == nil then parentEl.children = {} end object.isChild = true table.insert( parentEl.children, object ) end end end end -- SMW outputs a "flat" List of objects, after this the output is more or less equal to that from the API stratify( smwData ) return smwData end --- Creates the subtitle that is shown in the card --- --- Show info based on importance to readers --- When the first tier is not available, show the next tier --- --- @param item table Item i.e. row from the smw query --- @return string function methodtable.makeSubtitle( self, item ) local subtitle = {} -- Tier 1 -- Component-specific stats that affects gameplay -- SCU if item.scu ~= nil then -- Fix for german number format if string.find( item.scu, ',', 1, true ) then item.scu = string.gsub( item.scu, ',', '.' ) end if type( item.scu ) ~= 'number' then local success, scu = pcall( tonumber, item.scu, 10 ) if success then item.scu = scu end end -- We need to use raw value from SMW to show scu in different units (SCU, K µSCU) -- So we need to format the number manually if item.type == translate( 'CargoGrid' ) then table.insert( subtitle, common.formatNum( item.scu ) .. ' SCU' or 'N/A' ) elseif item.type == translate( 'PersonalStorage' ) then table.insert( subtitle, common.formatNum( item.scu * 1000 ) .. 'K µSCU' or 'N/A' ) end end -- Components that don't have a wiki page currently -- Magazine Capacity if item.magazine_capacity ~= nil then if type( item.magazine_capacity ) == 'table' then table.insert( subtitle, string.format( '%s/∞ %s', item.magazine_capacity[ 1 ], translate( 'Ammunition' ) ) ) else table.insert( subtitle, string.format( '%s/%s %s', item.magazine_capacity, item.magazine_capacity, translate( 'Ammunition' ) ) ) end end -- Parts if item.hp ~= nil then table.insert( subtitle, item.hp ) end -- Fuel tanks if item.fuel_capacity ~= nil then table.insert( subtitle, item.fuel_capacity ) end -- Fuel intake if item.fuel_intake_rate ~= nil then table.insert( subtitle, item.fuel_intake_rate ) end -- Self destruct if item.damage ~= nil and item.damage_radius ~= nil then table.insert( subtitle, string.format( '%s · %s', item.damage, item.damage_radius ) ) end -- Thrusters if item.thrust_capacity ~= nil then table.insert( subtitle, item.thrust_capacity ) end -- Weapon ports if item.type == translate( 'WeaponPort' ) then table.insert( subtitle, string.format( '%s (S%s – S%s)', translate( 'Weapon' ), item.min_size or 0, item.max_size or 0 ) ) end -- Items with Grade and/or Class if item.item_grade ~= nil or item.item_class ~= nil then local grade_class = '' -- TODO can't use lang suffix for subquery properties if type( item.item_class ) == 'table' then local parts = mw.text.split( item.item_class[ 1 ], ' (', true ) if #parts == 2 then grade_class = parts[ 1 ] item.item_class = parts[ 1 ] else grade_class = grade_class[ 1 ] item.item_class = item.item_class[ 1 ] end end if item.item_grade ~= nil and item.item_class ~= nil then grade_class = string.format( '%s (%s)', item.item_class, item.item_grade ) elseif item.item_grade ~= nil then grade_class = item.item_grade end table.insert( subtitle, grade_class ) end -- Tier 2 -- Info that might affect gameplay but not as important if next( subtitle ) == nil then -- Position if item.position ~= nil then if type( item.position ) ~= 'table' then item.position = { item.position } end local converted = {} for _, position in ipairs( item.position ) do table.insert( converted, mw.text.trim( mw.getContentLanguage():ucfirst( string.gsub( position, '_', ' ' ) ) ) ) end table.insert( subtitle, table.concat( converted, ', ' ) ) end end -- Tier 3 -- Info that does not affect gameplay if next( subtitle ) == nil then -- Manufacturer if item.manufacturer ~= nil and item.manufacturer ~= 'N/A' then table.insert( subtitle, string.format( '[[%s]]', item.manufacturer ) ) end end -- Return if there are no information at all if next( subtitle ) == nil then return '' end return table.concat( subtitle, ' · ' ) end --- Generate the output --- --- @param groupedData table Grouped SMW data --- @return table function methodtable.makeOutput( self, groupedData ) local classOutput = {} -- An item with potential children local function makeEntry( item, depth ) -- Info if data stems from ship-matrix or game files if classOutput.info == nil then local text if item.from_gamedata == true then text = translate( 'msg_from_gamedata' ) else text = translate( 'msg_from_shipmatrix' ) end classOutput.info = hatnote( text, { icon = 'WikimediaUI-Robot.svg' } ) end depth = depth or 1 local row = mw.html.create( 'div' ) :addClass( 'template-component' ) :addClass( string.format( 'template-component--level-%d', depth ) ) :tag( 'div' ) :addClass( 'template-component__connectors' ) :tag( 'div' ):addClass( 'template-component__connectorX' ):done() :tag( 'div' ):addClass( 'template-component__connectorY' ):done() :done() local size = 'N/A' local prefix = '' -- If Ship-Matrix components are not saved to SMW, always output the 'S' prefix if item.from_gamedata == nil then prefix = 'S' else if item.from_gamedata == true or item.from_gamedata == 1 or item.from_gamedata == '1' or -- For uninitialized attributes item.class == translate( 'Weapons' ) then prefix = 'S' end end if item.item_size ~= nil then size = string.format( '%s%s', prefix, item.item_size ) else size = string.format( '%s%s', prefix, item.max_size ) end local nodeSizeCount = mw.html.create( 'div' ) :addClass('template-component__port') :tag( 'div' ) :addClass( 'template-component__count' ) :wikitext( string.format( '%dx', item.count ) ) :done() if item.class ~= translate( 'CargoGrid' ) then nodeSizeCount :tag( 'div' ) :addClass( 'template-component__size' ) :wikitext( size ) :done() end nodeSizeCount = nodeSizeCount:allDone() local name = item.sub_type or item.type if item.name ~= nil then if config.name_fixes[ item.name ] ~= nil then name = string.format( '[[%s|%s]]', config.name_fixes[ item.name ], item.name ) else name = string.format( '[[%s]]', item.name ) end end local nodeItem = mw.html.create( 'div' ) :addClass( 'template-component__item' ) :tag( 'div' ) :addClass( 'template-component__title' ) :wikitext( name ) :done() local subtitle = self:makeSubtitle( item ) if subtitle ~= '' then nodeItem:tag( 'div' ) :addClass( 'template-component__subtitle' ) :wikitext( subtitle ) end row:tag( 'div' ) :addClass( 'template-component__card' ) :node( nodeSizeCount ) :node( nodeItem ) :done() row = tostring( row ) if type( item.children ) == 'table' then depth = depth + 1 for _, child in ipairs( item.children ) do row = row .. makeEntry( child, depth ) end end return row end -- Items of a given class e.g. avionics local function makeSection( types ) local out = '' for classType, items in common.spairs( types ) do local label = classType -- Label override -- Note: This must be manually changed on the data.json page if data.section_label_fixes[ classType ] ~= nil then label = data.section_label_fixes[ classType ] end local icon = '' if self.iconMap[ classType ] ~= nil then icon = string.format( '[[%s|20px|link=]]', self.iconMap[ classType ] ) end local section = mw.html.create( 'div' ) :addClass( 'template-components__section') :tag( 'div' ) :addClass( 'template-components__label' ) :wikitext( string.format( '%s %s', icon, classType ) ) :done() :tag( 'div' ):addClass( 'template-components__group' ) local str = '' for _, item in ipairs( items ) do if not item.isChild then local subGroup = mw.html.create( 'div' ) :addClass( 'template-components__subgroup' ) :node( makeEntry( item ) ) :allDone() str = str .. tostring( subGroup ) end end out = out .. tostring( section:node( str ):allDone() ) end return out end for class, types in common.spairs( groupedData ) do classOutput[ class ] = makeSection( types ) end mw.logObject( classOutput, 'makeOutput' ) return classOutput end --- Generates tabber output function methodtable.out( self ) local smwData = self:querySmwStore( self.page ) if smwData == nil then return hatnote( TNT.format( 'Module:VehicleHardpoint/i18n.json', 'msg_no_data', self.page ), { icon = 'WikimediaUI-Error.svg' } ) end smwData = self:createDataStructure( smwData ) smwData = self:group( smwData ) local output = self:makeOutput( smwData ) local tabberData = {} for i, grouping in ipairs( data.class_groupings ) do local key = grouping[ 1 ] local groups = grouping[ 2 ] local groupContent = '' local label = {} for _, group in ipairs( groups ) do groupContent = groupContent .. ( output[ translate( group ) ] or '' ) table.insert( label, translate( group ) ) end if #groupContent == 0 then groupContent = translate( 'empty_' .. key ) end tabberData[ 'label' .. i ] = table.concat( label, ' & ' ) tabberData[ 'content' .. i ] = groupContent end return require( 'Module:Tabber' ).renderTabber( tabberData ) .. mw.getCurrentFrame():extensionTag{ name = 'templatestyles', args = { src = config.template_styles_page } } end --- Generates debug output function methodtable.makeDebugOutput( self ) local debug = require( 'Module:Common/Debug' ) self.smwData = nil local smwData = self:querySmwStore( self.page ) local struct = self:createDataStructure( smwData or {} ) local group = self:group( struct ) return debug.collapsedDebugSections({ { title = 'SMW Query', content = debug.convertSmwQueryObject( makeSmwQueryObject( self.page ) ), }, { title = 'SMW Data', content = smwData, tag = 'pre', }, { title = 'Datastructure', content = struct, tag = 'pre', }, { title = 'Grouped', content = group, tag = 'pre', }, { title = 'Output', content = self:makeOutput( group ), tag = 'pre', }, }) end --- Manually fix some (sub_)types by checking the hardpoint name --- --- @param hardpoint table Entry from the api --- @param fixes table --- @return table The fixed entry function VehicleHardpoint.fixTypes( hardpoint, fixes ) --- Assign key value pairs on a hardpoint --- @param kv table Table containing 'key=value' string pairs local function assign( kv ) for _, assignment in pairs( kv ) do local parts = mw.text.split( assignment, '=', true ) if #parts == 2 then if string.find( parts[ 2 ], '+', 1, true ) then local valueParts = mw.text.split( parts[ 2 ], '+', true ) parts[ 2 ] = valueParts[ 1 ] .. ( hardpoint[ valueParts[ 2 ] ] or '' ) end hardpoint[ parts[ 1 ] ] = parts[ 2 ] end end end --- Set fixes on a hardpoint if tests evaluate to true --- @param tests table local function fixHardpoint( tests ) for _, test in ipairs( tests ) do if VehicleHardpoint.evalRule( test[ 'if' ], hardpoint ) then local kv = test[ 'then' ] if type( kv ) ~= 'table' then kv = { kv } end assign( kv ) end end end for _, fix in ipairs( fixes ) do if type( fix.type ) == 'table' then for _, v in pairs( fix.type ) do if v == hardpoint.type then fixHardpoint( fix.modification ) break end end elseif type( fix.type ) == 'string' and fix.type == hardpoint.type then fixHardpoint( fix.modification ) break end end -- Manual mapping defined in Module:VehicleHardpoint/Data if type( hardpoint.item ) == 'table' and hardpoint.item ~= nil then -- If this is a noise launcher, but the class name says decoy, change Noise to Decoy if string.find( hardpoint.item.name, 'Noise', 1, true ) and string.find( hardpoint.class_name, 'decoy', 1, true ) then hardpoint.item.name = string.gsub( hardpoint.item.name, ' Noise ', ' Decoy ' ) end for _, mapping in pairs( data.hardpoint_type_fixes ) do for _, matcher in pairs( data.matches[ mapping ][ 'matches' ] ) do if string.match( hardpoint.name, matcher ) ~= nil then hardpoint.type = mapping return hardpoint end end end end return hardpoint end --- New Instance --- --- @return table VehicleHardpoint function VehicleHardpoint.new( self, page ) local instance = { page = page or nil, iconMap = {} } setmetatable( instance, metatable ) return instance end --- Parser call for generating the table function VehicleHardpoint.outputTable( frame ) local args = require( 'Module:Arguments' ).getArgs( frame ) local page = args[ 1 ] or args[ 'Name' ] or mw.title.getCurrentTitle().rootText local instance = VehicleHardpoint:new( page ) local out = instance:out() local debugOutput = '' if args['debug'] ~= nil then debugOutput = instance:makeDebugOutput() end return out .. debugOutput end --- Set the hardpoints of the 300i as subobjects to the current page function VehicleHardpoint.test( frame ) local page = frame.args['Name'] or '300i' local json = mw.text.jsonDecode( mw.ext.Apiunto.get_raw( 'v2/vehicles/' .. page, { include = { 'hardpoints', 'parts' }, } ) ) local hardpoint = VehicleHardpoint:new( page ) hardpoint:setHardPointObjects( json.data.hardpoints ) hardpoint:setParts( json.data.parts ) end --- Evaluates rules from 'data.fixes' --- --- @param rules table A rules object from data.fixes --- @param hardpoint table The hardpoint to evaluate --- @param returnInvalid boolean|nil If invalid rules should be returned beneath the result --- @return boolean (, table) function VehicleHardpoint.evalRule( rules, hardpoint, returnInvalid ) returnInvalid = returnInvalid or false local stepVal = {} local combination = {} local invalidRules = {} local function invalidRule( rule, index ) table.insert( invalidRules, string.format( 'Invalid Rule found, skipping: <%s (Element %d)>', rule, index ) ) end for index, rule in ipairs( rules ) do if type( rule ) == 'string' then -- mw.logObject( string.format( 'Evaluating rule %s', rule ), 'evalRule' ) if string.find( rule, ':', 1, true ) ~= nil then local parts = mw.text.split( rule, ':', true ) -- Simple check if a key equals a value if #parts == 2 then local result = hardpoint[ parts[ 1 ] ] == parts[ 2 ] -- mw.logObject( string.format( 'Rule <%s == %s>, equates to %s', hardpoint[ parts[ 1 ] ], parts[ 2 ], tostring( result ) ), 'evalRule' ) table.insert( stepVal, result ) -- String Match elseif #parts == 3 then local key = parts[ 1 ] local fn = parts[ 2 ] -- Remove key and 'match' in order to combine the last parts again table.remove( parts, 1 ) table.remove( parts, 1 ) local matcher = mw.ustring.lower( table.concat( parts, ':' ) ) local result = string[ fn ]( string.lower( hardpoint[ key ] ), matcher ) ~= nil -- mw.logObject( string.format( 'Rule <%s matches %s>, equates to %s', hardpoint[ key ], matcher, tostring( result ) ), 'evalRule' ) table.insert( stepVal, result ) else invalidRule( rule, index ) end -- A combination rule elseif rule == 'and' or rule == 'or' then table.insert( combination, rule ) end -- A sub rule elseif type( rule ) == 'table' then local matches, invalid = VehicleHardpoint.evalRule( rule, hardpoint ) table.insert( stepVal, matches ) for _, v in ipairs( invalid or {} ) do table.insert( invalidRules, v ) end else -- mw.logObject( 'Is invalid ' .. rule, 'evalRule' ) invalidRule( rule, index ) end end local ruleMatches = false for index, matched in ipairs( stepVal ) do if index == 1 then ruleMatches = matched else -- mw.logObject( 'test is ' .. combination[ index - 1 ], 'evalRule' ) if combination[ index - 1 ] == 'and' then ruleMatches = ruleMatches and matched else ruleMatches = ruleMatches or matched end end end -- mw.logObject( 'Final rule result is ' .. tostring( ruleMatches ), 'evalRule' ) if returnInvalid then return ruleMatches, invalidRules else return ruleMatches end end return VehicleHardpoint