Skip to content

Commit

Permalink
Merge pull request #6013 from sturnclaw/hull-validation-nonempty
Browse files Browse the repository at this point in the history
Add support for required slots, validate shipdefs on startup
  • Loading branch information
sturnclaw authored Jan 14, 2025
2 parents fa7567e + 582bdf2 commit 80c06a3
Show file tree
Hide file tree
Showing 19 changed files with 540 additions and 45 deletions.
1 change: 1 addition & 0 deletions data/libs/EquipSet.lua
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,7 @@ end
---@param slotHandle HullConfig.Slot?
---@return boolean
function EquipSet:Install(equipment, slotHandle)
assert(equipment:isInstance())
local slotId = self.idCache[slotHandle]

if slotHandle then
Expand Down
9 changes: 9 additions & 0 deletions data/libs/EquipType.lua
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,14 @@ function EquipType:isProto()
return not rawget(self, "__proto")
end

-- Method: isInstance
--
-- Returns true if this object is an equipment item instance, false if it is
-- n prototype.
function EquipType:isInstance()
return rawget(self, "__proto") ~= nil
end

-- Method: GetPrototype
--
-- Return the prototype this equipment item instance is derived from, or the
Expand Down Expand Up @@ -190,6 +198,7 @@ end
-- instance represents.
---@param count integer
function EquipType:SetCount(count)
assert(self:isInstance())
local proto = self:GetPrototype()

self.mass = proto.mass * count
Expand Down
22 changes: 22 additions & 0 deletions data/libs/Event.lua
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,28 @@ Event.New = function()
return self
end

--
-- Event: onEnterMainMenu
--
-- Triggered when the menu is loaded.
--
-- > local onMenuLoaded = function () ... end
-- > Event.Register("onEnterMainMenu", onMenuLoaded)
--
-- onMenuLoaded is triggered once the menu is fully available
--
-- This is a good place to perform startup checks, including checking for
-- errors and making them visible to the player
--
-- Availability:
--
-- 2025-02-03
--
-- Status:
--
-- stable
--

--
-- Event: onGameStart
--
Expand Down
3 changes: 3 additions & 0 deletions data/libs/HullConfig.lua
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Slot.size = 1
Slot.size_min = nil ---@type number?
Slot.tag = nil ---@type string?
Slot.default = nil ---@type string?
Slot.required = false ---@type boolean
Slot.hardpoint = false
Slot.i18n_key = nil ---@type string?
Slot.i18n_res = "equipment-core"
Expand All @@ -41,6 +42,7 @@ Slot.gimbal = nil ---@type table?
local HullConfig = utils.proto("HullConfig")

HullConfig.id = ""
HullConfig.path = ""
HullConfig.equipCapacity = 0

-- Default slot config for a new shipdef
Expand Down Expand Up @@ -71,6 +73,7 @@ local function CreateShipConfig(def)
Serializer:RegisterPersistent("ShipDef." .. def.id, newShip)

newShip.id = def.id
newShip.path = def.path
newShip.equipCapacity = def.equipCapacity

table.merge(newShip.slots, def.raw.equipment_slots or {}, function(name, slotDef)
Expand Down
18 changes: 18 additions & 0 deletions data/libs/utils.lua
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,24 @@ local Engine = require 'Engine' -- rand
--
local utils = {}

--
-- Function: keys
--
-- Create an iterator that returns a numberic index and the keys of the
-- provided table. Iteration order is undefined (uses pairs() internally).
--
-- Example:
-- > for i, k in utils.keys(table) do ... end
--
function utils.keys(table)
local k = nil
local f = function(s, i)
k = next(s, k)
return (k and i + 1 or nil), k
end
return f, table, 0
end

--
-- Function: numbered_keys
--
Expand Down
1 change: 1 addition & 0 deletions data/meta/ShipDef.lua
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

---@class ShipDef
---@field id string
---@field path string
---@field name string
---@field shipClass string
---@field manufacturer string
Expand Down
148 changes: 148 additions & 0 deletions data/modules/Debug/CheckShipData.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
-- Copyright © 2008-2025 Pioneer Developers. See AUTHORS.txt for details
-- Licensed under the terms of the GPL v3. See licenses/GPL-3.txt

local Equipment = require 'Equipment'
local HullConfig = require 'HullConfig'
local Loader = require '.DebugLoader'
local EquipSet = require 'EquipSet'
local Lang = require 'Lang'
local ShipDef = require 'ShipDef'

local utils = require 'utils'

-- This file implements validation passes for ship JSON files
-- It's intended to catch most common errors, especially those that would be
-- difficult to find outside of switching to each ship type in sequence.

local activeFile = nil

local error = function(message) Loader.LogFileMessage(Loader.Type.Error, activeFile, message) end
local warn = function(message) Loader.LogFileMessage(Loader.Type.Warn, activeFile, message) end
local info = function(message) Loader.LogFileMessage(Loader.Type.Info, activeFile, message) end

local function findMatchingSlots(config, type)
return utils.filter_table(config.slots, function(_, slot)
return EquipSet.SlotTypeMatches(slot.type, type)
end)
end

---@param slot HullConfig.Slot
local function checkSlot(slot)

if string.match(slot.id, "##") then
error("Slot {id} name contains invalid sequence '##'." % slot)
end

if not string.match(slot.id, "^[a-zA-Z0-9_]+$") then
warn("Slot {id} name contains non-identifier characters." % slot)
end

if slot.required and not slot.default then
error("Slot {id} is a required slot but does not have a default equipment item." % slot)
end

if slot.default and not Equipment.Get(slot.default) then
error("Slot {id} default item ({default}) does not exist." % slot)
end

if EquipSet.SlotTypeMatches(slot.type, "hyperdrive") and not slot.default then
warn("Slot {id} has no default hyperdrive equipment." % slot)
end

if slot.i18n_key then
if not slot.i18n_res then
error("Slot {id} has an invalid language resource key {i18n_res}." % slot)
end

local res = Lang.GetResource(slot.i18n_res)

if not rawget(res, slot.i18n_key) then
warn("Slot {id} uses undefined lang string '{i18n_res}.{i18n_key}'." % slot)
end
end

local isWeaponType = EquipSet.SlotTypeMatches(slot.type, "weapon")
local isPylonType = EquipSet.SlotTypeMatches(slot.type, "pylon")
local isBayType = EquipSet.SlotTypeMatches(slot.type, "missile_bay")
local isScoopType = EquipSet.SlotTypeMatches(slot.type, "fuel_scoop")

local isExternal = isWeaponType or isPylonType or isBayType or isScoopType

if isExternal then

if not slot.hardpoint then
error("External slot {id} with type {type} should have hardpoint=true." % slot)
end

if not slot.tag then
info("External slot {id} with type {type} is missing an associated tag." % slot)
end

end

if isWeaponType then

if not slot.gimbal or type(slot.gimbal) ~= "table" then
error("Weapon slot {id} is missing gimbal data." % slot)
elseif type(slot.gimbal[1]) ~= "number" or type(slot.gimbal[2]) ~= "number" then
error("Weapon slot {id} should have a two-axis gimbal expressed as [x, y]." % slot)
end

end

end

---@param config HullConfig
local function checkConfig(config)
if utils.count(findMatchingSlots(config, "hyperdrive")) > 1 then
error("Ship {id} has more than one hyperdrive slot; this will break module code." % config)
end

if utils.count(findMatchingSlots(config, "thruster")) == 0 then
warn("Ship {id} has no thruster slots. This may break in the future.")
end

-- TODO: more validation passes on the whole ship config
end

---@param shipDef ShipDef
local function checkShipDef(shipDef)
if shipDef.tag ~= "SHIP" then
return
end

if utils.count(shipDef.roles) == 0 then
info("Ship {id} has no roles and will not be used by most modules." % shipDef)
end

if shipDef.minCrew > shipDef.maxCrew then
error("Ship {id} has minCrew {minCrew} > maxCrew {maxCrew}." % shipDef)
end

if not shipDef.shipClass or shipDef.shipClass == "" then
warn("Ship {id} has invalid/empty ship_class field." % shipDef)
end

if not shipDef.manufacturer then
info("Ship {id} has no manufacturer set." % shipDef)
end
end

Loader.RegisterCheck("HullConfigs", function()

local configs = HullConfig.GetHullConfigs()

for _, config in pairs(configs) do
activeFile = config.path

for _, slot in pairs(config.slots) do checkSlot(slot) end
checkConfig(config)
end

for _, shipDef in pairs(ShipDef) do
activeFile = shipDef.path

checkShipDef(shipDef)
end

end)
Loading

0 comments on commit 80c06a3

Please sign in to comment.