I spent a few minutes writing out an example parser:
<?php
$msg = "Hello [\rimg\f \aalt\z="\nHello World\n" \atitle\z=\nHi\n thisisempty\n\n\r]\qworld.png\t[/img]\t";
$nextChar = strpos($msg, "\r");
if ($nextChar === false) {
    cleanString($msg);
}
$newMsg = '';
// I don't think we need a do/while, actually. A while should work.
do {
    $tagEndPosition = strpos($msg, "\f", $nextChar);
    $tag = substr($msg, $nextChar, $tagEndPosition - $nextChar);
    if (isset($bbc[$tag])) {
        $checkCodes = $bbc[$tag];
        
        // Find the next \r
        $paramStringEndPos = strpos($msg, "\r", $tagEndPosition);
        
        // If the next \r is not 0 or 1 difference:
        // Find the next \a
        $paramEndPos = $tagEndPosition;
        $params = [];
        while(false !== ($paramPos = strpos($msg, "\a", $paramEndPos))) {
            // Find the param
            $paramEndPos = strpos($msg, "\z", $paramPos);
            $param = substr($msg, $paramPos, $paramEndPos - $paramPos);
            
            // Get the value (empty values use \n\n to be easy)
            $valuePos = strpos($msg, "\n", $paramEndPos);
            $valueEndPos = strpos($msg, "\n", $valuePos);
            $value = substr($msg, $valuePos, $valueEndPos - $valuePos);
            
            // Comma delimited strings are a bit different, but their key would just be 1, 2, 3, etc.
            $params[$param] = $value;
        }
        
        ksort($params);
        
        $foundCode = false;
        foreach ($checkCodes as $code) {
            if (!checkRequiredParameters($code, $params)) {
                continue;
            }
            
            $optionalParameters = array_diff_key($code->getRequiredParameters(), $params);
            
            if (!checkOptionalParameters($code, $optionalParameters)) {
                continue;
            }
            
            // Cool... now we have a tag and parameters.
            
            // Next step is to get any content it has. You can guess how that will go.
            // Finally, we parse the code.
            parseCode($code, $params, $content);
        }
        
        $nextChar = $foundCode ? $closingTagClosingPos : $valueEndPos;
    } else {
        cleanString($msg);
    }
} while ($nextChar = strpos($msg, "\r"));
function checkRequiredParameters($code, array $params)
{
    foreach ($code->getRequiredParameters() as $requiredParameter) {
        if (!isset($params[$requiredParameter])) {
            return false;
        }
    }
    
    return true;
}
function checkOptionalParameters($code, array $params)
{
    return array_intersect($params, $code->getOptionalParameters()) !== array();
}
// Clean any remaining special characters
function cleanString($msg)
{
    return str_replace(["\r", "\f", "\a", "\n"], '', $msg);
}