---@generic T
---@param shape T | fun()
---@param process fun(entity: T, dt: number, system: System) | nil
---@param compare nil | fun(system: System, entityA: T, entityB: T): boolean
---@return System | { entities: T[] }
function filteredSystem(name, shape, process, compare)
    assert(type(name) == "string")
    assert(type(shape) == "table" or type(shape) == "function")
    assert(process == nil or type(process) == "function")

    local system = compare and tiny.sortedProcessingSystem() or tiny.processingSystem()
    system.compare = compare
    system.name = name
    if type(shape) == "table" then
        local keys = {}
        for key, value in pairs(shape) do
            local isTable = type(value) == "table"
            local isMaybe = isTable and value.maybe ~= nil

            if not isMaybe then
                -- ^ Don't require any Maybe types
                keys[#keys + 1] = key
            end
        end
        system.filter = tiny.requireAll(unpack(keys))
    elseif type(shape) == "function" then
        system.filter = shape
    end
    if not process then
        return World:addSystem(system)
    end
    function system:process(e, dt)
        process(e, dt, self)
    end
    return World:addSystem(system)
end