Utility/OBJParser: Optimized loading

Former-commit-id: f84e73928d0596d5933cadea572465ded864192f [formerly 0621b0e5e8f674273190ed08e5d984c085d190a9]
Former-commit-id: 7dae4cbeff3644cdb0246ac3d077ddcb5bc7b51f
This commit is contained in:
Lynix 2016-07-07 09:00:35 +02:00
parent 88e337cb73
commit 84e9b3e148
4 changed files with 222 additions and 191 deletions

View File

@ -29,51 +29,59 @@ namespace Nz
struct Face struct Face
{ {
std::vector<FaceVertex> vertices; std::size_t firstVertex;
std::size_t vertexCount;
}; };
struct Mesh struct Mesh
{ {
std::vector<Face> faces; std::vector<Face> faces;
std::vector<FaceVertex> vertices;
String name; String name;
unsigned int material; std::size_t material;
}; };
OBJParser(Stream& stream$); OBJParser() = default;
~OBJParser(); ~OBJParser() = default;
const String* GetMaterials() const; inline const String* GetMaterials() const;
unsigned int GetMaterialCount() const; inline unsigned int GetMaterialCount() const;
const Mesh* GetMeshes() const; inline const Mesh* GetMeshes() const;
unsigned int GetMeshCount() const; inline unsigned int GetMeshCount() const;
const String& GetMtlLib() const; inline const String& GetMtlLib() const;
const Vector3f* GetNormals() const; inline const Vector3f* GetNormals() const;
unsigned int GetNormalCount() const; inline unsigned int GetNormalCount() const;
const Vector4f* GetPositions() const; inline const Vector4f* GetPositions() const;
unsigned int GetPositionCount() const; inline unsigned int GetPositionCount() const;
const Vector3f* GetTexCoords() const; inline const Vector3f* GetTexCoords() const;
unsigned int GetTexCoordCount() const; inline unsigned int GetTexCoordCount() const;
bool Parse(std::size_t reservedVertexCount = 100); bool Parse(Stream& stream, std::size_t reservedVertexCount = 100);
private: private:
bool Advance(bool required = true); bool Advance(bool required = true);
void Error(const String& message); template<typename T> void Emit(const T& text) const;
void Warning(const String& message); inline void EmitLine() const;
void UnrecognizedLine(bool error = false); template<typename T> void EmitLine(const T& line) const;
inline void Error(const String& message);
inline void Flush() const;
inline void Warning(const String& message);
inline void UnrecognizedLine(bool error = false);
std::vector<Mesh> m_meshes; std::vector<Mesh> m_meshes;
std::vector<String> m_materials; std::vector<String> m_materials;
std::vector<Vector3f> m_normals; std::vector<Vector3f> m_normals;
std::vector<Vector4f> m_positions; std::vector<Vector4f> m_positions;
std::vector<Vector3f> m_texCoords; std::vector<Vector3f> m_texCoords;
Stream& m_stream; mutable Stream* m_currentStream;
String m_currentLine; String m_currentLine;
String m_mtlLib; String m_mtlLib;
mutable StringStream m_outputStream;
bool m_keepLastLine; bool m_keepLastLine;
unsigned int m_lineCount; unsigned int m_lineCount;
unsigned int m_streamFlags;
}; };
} }
#include <Nazara/Utility/Formats/OBJParser.inl>
#endif // NAZARA_FORMATS_OBJPARSER_HPP #endif // NAZARA_FORMATS_OBJPARSER_HPP

View File

@ -0,0 +1,88 @@
// Copyright (C) 2016 Jérôme Leclercq
// This file is part of the "Nazara Engine - Utility module"
// For conditions of distribution and use, see copyright notice in Config.hpp
#include <Nazara/Utility/Formats/OBJParser.hpp>
#include <Nazara/Core/Error.hpp>
#include <Nazara/Utility/Debug.hpp>
namespace Nz
{
inline const String* OBJParser::GetMaterials() const
{
return m_materials.data();
}
inline unsigned int OBJParser::GetMaterialCount() const
{
return m_materials.size();
}
inline const OBJParser::Mesh* OBJParser::GetMeshes() const
{
return m_meshes.data();
}
inline unsigned int OBJParser::GetMeshCount() const
{
return m_meshes.size();
}
inline const String& OBJParser::GetMtlLib() const
{
return m_mtlLib;
}
inline const Vector3f* OBJParser::GetNormals() const
{
return m_normals.data();
}
inline unsigned int OBJParser::GetNormalCount() const
{
return m_normals.size();
}
inline const Vector4f* OBJParser::GetPositions() const
{
return m_positions.data();
}
inline unsigned int OBJParser::GetPositionCount() const
{
return m_positions.size();
}
inline const Vector3f* OBJParser::GetTexCoords() const
{
return m_texCoords.data();
}
inline unsigned int OBJParser::GetTexCoordCount() const
{
return m_texCoords.size();
}
inline void OBJParser::Error(const String& message)
{
NazaraError(message + " at line #" + String::Number(m_lineCount));
}
inline void OBJParser::Warning(const String& message)
{
NazaraWarning(message + " at line #" + String::Number(m_lineCount));
}
inline void OBJParser::UnrecognizedLine(bool error)
{
String message = "Unrecognized \"" + m_currentLine + '"';
if (error)
Error(message);
else
Warning(message);
}
}
#include <Nazara/Utility/DebugOff.hpp>

View File

@ -123,8 +123,8 @@ namespace Nz
if (!parameters.custom.GetIntegerParameter("NativeOBJLoader_VertexCount", &reservedVertexCount)) if (!parameters.custom.GetIntegerParameter("NativeOBJLoader_VertexCount", &reservedVertexCount))
reservedVertexCount = 100; reservedVertexCount = 100;
OBJParser parser(stream); OBJParser parser;
if (!parser.Parse(reservedVertexCount)) if (!parser.Parse(stream, reservedVertexCount))
{ {
NazaraError("OBJ parser failed"); NazaraError("OBJ parser failed");
return false; return false;
@ -177,23 +177,24 @@ namespace Nz
{ {
bool operator()(const OBJParser::FaceVertex& lhs, const OBJParser::FaceVertex& rhs) const bool operator()(const OBJParser::FaceVertex& lhs, const OBJParser::FaceVertex& rhs) const
{ {
return lhs.normal == rhs.normal && return lhs.normal == rhs.normal &&
lhs.position == rhs.position && lhs.position == rhs.position &&
lhs.texCoord == rhs.texCoord; lhs.texCoord == rhs.texCoord;
} }
}; };
std::unordered_map<OBJParser::FaceVertex, unsigned int, FaceVertexHasher, FaceVertexComparator> vertices; std::unordered_map<OBJParser::FaceVertex, unsigned int, FaceVertexHasher, FaceVertexComparator> vertices;
vertices.reserve(meshes[i].vertices.size());
unsigned int vertexCount = 0; unsigned int vertexCount = 0;
for (unsigned int j = 0; j < faceCount; ++j) for (unsigned int j = 0; j < faceCount; ++j)
{ {
unsigned int faceVertexCount = meshes[i].faces[j].vertices.size(); unsigned int faceVertexCount = meshes[i].faces[j].vertexCount;
faceIndices.resize(faceVertexCount); faceIndices.resize(faceVertexCount);
for (unsigned int k = 0; k < faceVertexCount; ++k) for (unsigned int k = 0; k < faceVertexCount; ++k)
{ {
const OBJParser::FaceVertex& vertex = meshes[i].faces[j].vertices[k]; const OBJParser::FaceVertex& vertex = meshes[i].vertices[meshes[i].faces[j].firstVertex + k];
auto it = vertices.find(vertex); auto it = vertices.find(vertex);
if (it == vertices.end()) if (it == vertices.end())
@ -202,6 +203,7 @@ namespace Nz
faceIndices[k] = it->second; faceIndices[k] = it->second;
} }
// Triangulation
for (unsigned int k = 1; k < faceVertexCount-1; ++k) for (unsigned int k = 1; k < faceVertexCount-1; ++k)
{ {
indices.push_back(faceIndices[0]); indices.push_back(faceIndices[0]);
@ -233,17 +235,17 @@ namespace Nz
MeshVertex& vertex = meshVertices[index]; MeshVertex& vertex = meshVertices[index];
const Vector4f& vec = positions[vertexIndices.position]; const Vector4f& vec = positions[vertexIndices.position-1];
vertex.position = Vector3f(parameters.matrix * vec); vertex.position = Vector3f(parameters.matrix * vec);
if (vertexIndices.normal >= 0) if (vertexIndices.normal > 0)
vertex.normal = normals[vertexIndices.normal]; vertex.normal = normals[vertexIndices.normal-1];
else else
hasNormals = false; hasNormals = false;
if (vertexIndices.texCoord >= 0) if (vertexIndices.texCoord > 0)
{ {
const Vector3f& uvw = texCoords[vertexIndices.texCoord]; const Vector3f& uvw = texCoords[vertexIndices.texCoord-1];
vertex.uv.Set(uvw.x, (parameters.flipUVs) ? 1.f - uvw.y : uvw.y); // Inversion des UVs si demandé vertex.uv.Set(uvw.x, (parameters.flipUVs) ? 1.f - uvw.y : uvw.y); // Inversion des UVs si demandé
} }
else else

View File

@ -3,7 +3,7 @@
// For conditions of distribution and use, see copyright notice in Config.hpp // For conditions of distribution and use, see copyright notice in Config.hpp
#include <Nazara/Utility/Formats/OBJParser.hpp> #include <Nazara/Utility/Formats/OBJParser.hpp>
#include <Nazara/Core/Error.hpp> #include <Nazara/Core/CallOnExit.hpp>
#include <Nazara/Core/Log.hpp> #include <Nazara/Core/Log.hpp>
#include <Nazara/Utility/Config.hpp> #include <Nazara/Utility/Config.hpp>
#include <cctype> #include <cctype>
@ -13,77 +13,22 @@
namespace Nz namespace Nz
{ {
OBJParser::OBJParser(Stream& stream) : bool OBJParser::Parse(Nz::Stream& stream, std::size_t reservedVertexCount)
m_stream(stream),
m_streamFlags(stream.GetStreamOptions()) //< Saves stream flags
{ {
m_stream.EnableTextMode(true); m_currentStream = &stream;
}
OBJParser::~OBJParser() // Force stream in text mode, reset it at the end
{ Nz::CallOnExit resetTextMode;
// Reset stream flags if ((stream.GetStreamOptions() & StreamOption_Text) == 0)
if ((m_streamFlags & StreamOption_Text) == 0) {
m_stream.EnableTextMode(false); stream.EnableTextMode(true);
}
const String* OBJParser::GetMaterials() const resetTextMode.Reset([&stream] ()
{ {
return m_materials.data(); stream.EnableTextMode(false);
} });
}
unsigned int OBJParser::GetMaterialCount() const
{
return m_materials.size();
}
const OBJParser::Mesh* OBJParser::GetMeshes() const
{
return m_meshes.data();
}
unsigned int OBJParser::GetMeshCount() const
{
return m_meshes.size();
}
const String& OBJParser::GetMtlLib() const
{
return m_mtlLib;
}
const Vector3f* OBJParser::GetNormals() const
{
return m_normals.data();
}
unsigned int OBJParser::GetNormalCount() const
{
return m_normals.size();
}
const Vector4f* OBJParser::GetPositions() const
{
return m_positions.data();
}
unsigned int OBJParser::GetPositionCount() const
{
return m_positions.size();
}
const Vector3f* OBJParser::GetTexCoords() const
{
return m_texCoords.data();
}
unsigned int OBJParser::GetTexCoordCount() const
{
return m_texCoords.size();
}
bool OBJParser::Parse(std::size_t reservedVertexCount)
{
String matName, meshName; String matName, meshName;
matName = meshName = "default"; matName = meshName = "default";
m_keepLastLine = false; m_keepLastLine = false;
@ -100,32 +45,56 @@ namespace Nz
m_positions.reserve(reservedVertexCount); m_positions.reserve(reservedVertexCount);
m_texCoords.reserve(reservedVertexCount); m_texCoords.reserve(reservedVertexCount);
// On va regrouper les meshs par nom et par matériau // Sort meshes by material and group
using FaceVec = std::vector<Face>; using MatPair = std::pair<Mesh, unsigned int>;
using MatPair = std::pair<FaceVec, unsigned int>; std::unordered_map<String, std::unordered_map<String, MatPair>> meshesByName;
std::unordered_map<String, std::unordered_map<String, MatPair>> meshes;
unsigned int matIndex = 0; std::size_t faceReserve = 0;
auto GetMaterial = [&meshes, &matIndex] (const String& mesh, const String& material) -> FaceVec* std::size_t vertexReserve = 0;
unsigned int matCount = 0;
auto GetMaterial = [&] (const String& meshName, const String& matName) -> Mesh*
{ {
auto& map = meshes[mesh]; auto& map = meshesByName[meshName];
auto it = map.find(material); auto it = map.find(matName);
if (it == map.end()) if (it == map.end())
it = map.insert(std::make_pair(material, MatPair(FaceVec(), matIndex++))).first; it = map.insert(std::make_pair(matName, MatPair(Mesh(), matCount++))).first;
Mesh& mesh = it->second.first;
mesh.faces.reserve(faceReserve);
mesh.vertices.reserve(vertexReserve);
faceReserve = 0;
vertexReserve = 0;
return &(it->second.first); return &(it->second.first);
}; };
// On prépare le mesh par défaut // On prépare le mesh par défaut
FaceVec* currentMesh = nullptr; Mesh* currentMesh = nullptr;
while (Advance(false)) while (Advance(false))
{ {
switch (std::tolower(m_currentLine[0])) switch (std::tolower(m_currentLine[0]))
{ {
case 'f': // Une face case '#': //< Comment
// Some softwares write comments to gives the number of vertex/faces an importer can expect
std::size_t data;
if (std::sscanf(m_currentLine.GetConstBuffer(), "# position count: %zu", &data) == 1)
m_positions.reserve(data);
else if (std::sscanf(m_currentLine.GetConstBuffer(), "# normal count: %zu", &data) == 1)
m_normals.reserve(data);
else if (std::sscanf(m_currentLine.GetConstBuffer(), "# texcoords count: %zu", &data) == 1)
m_texCoords.reserve(data);
else if (std::sscanf(m_currentLine.GetConstBuffer(), "# face count: %zu", &data) == 1)
faceReserve = data;
else if (std::sscanf(m_currentLine.GetConstBuffer(), "# vertex count: %zu", &data) == 1)
vertexReserve = data;
break;
case 'f': //< Face
{ {
if (m_currentLine.GetSize() < 7) // Le minimum syndical pour définir une face de trois sommets (f 1 2 3) if (m_currentLine.GetSize() < 7) // Since we only treat triangles, this is the minimum length of a face line (f 1 2 3)
{ {
#if NAZARA_UTILITY_STRICT_RESOURCE_PARSING #if NAZARA_UTILITY_STRICT_RESOURCE_PARSING
UnrecognizedLine(); UnrecognizedLine();
@ -142,17 +111,23 @@ namespace Nz
break; break;
} }
if (!currentMesh)
currentMesh = GetMaterial(meshName, matName);
Face face; Face face;
face.vertices.resize(vertexCount); face.firstVertex = currentMesh->vertices.size();
face.vertexCount = vertexCount;
currentMesh->vertices.resize(face.firstVertex + vertexCount, FaceVertex{0, 0, 0});
bool error = false; bool error = false;
unsigned int pos = 2; unsigned int pos = 2;
for (unsigned int i = 0; i < vertexCount; ++i) for (unsigned int i = 0; i < vertexCount; ++i)
{ {
int offset; int offset;
int& n = face.vertices[i].normal; int& n = currentMesh->vertices[face.firstVertex + i].normal;
int& p = face.vertices[i].position; int& p = currentMesh->vertices[face.firstVertex + i].position;
int& t = face.vertices[i].texCoord; int& t = currentMesh->vertices[face.firstVertex + i].texCoord;
if (std::sscanf(&m_currentLine[pos], "%d/%d/%d%n", &p, &t, &n, &offset) != 3) if (std::sscanf(&m_currentLine[pos], "%d/%d/%d%n", &p, &t, &n, &offset) != 3)
{ {
@ -168,22 +143,13 @@ namespace Nz
error = true; error = true;
break; break;
} }
else
{
n = 0;
t = 0;
}
} }
else
n = 0;
} }
else
t = 0;
} }
if (p < 0) if (p < 0)
{ {
p += m_positions.size(); p += m_positions.size() - 1;
if (p < 0) if (p < 0)
{ {
Error("Vertex index out of range (" + String::Number(p) + " < 0"); Error("Vertex index out of range (" + String::Number(p) + " < 0");
@ -191,48 +157,42 @@ namespace Nz
break; break;
} }
} }
else
p--;
if (n < 0) if (n < 0)
{ {
n += m_normals.size(); n += m_normals.size() - 1;
if (n < 0) if (n < 0)
{ {
Error("Vertex index out of range (" + String::Number(n) + " < 0"); Error("Normal index out of range (" + String::Number(n) + " < 0");
error = true; error = true;
break; break;
} }
} }
else
n--;
if (t < 0) if (t < 0)
{ {
t += m_texCoords.size(); t += m_texCoords.size() - 1;
if (t < 0) if (t < 0)
{ {
Error("Vertex index out of range (" + String::Number(t) + " < 0"); Error("Texture coordinates index out of range (" + String::Number(t) + " < 0");
error = true; error = true;
break; break;
} }
} }
else
t--;
if (static_cast<unsigned int>(p) >= m_positions.size()) if (static_cast<std::size_t>(p) > m_positions.size())
{ {
Error("Vertex index out of range (" + String::Number(p) + " >= " + String::Number(m_positions.size()) + ')'); Error("Vertex index out of range (" + String::Number(p) + " >= " + String::Number(m_positions.size()) + ')');
error = true; error = true;
break; break;
} }
else if (n >= 0 && static_cast<unsigned int>(n) >= m_normals.size()) else if (n != 0 && static_cast<std::size_t>(n) > m_normals.size())
{ {
Error("Normal index out of range (" + String::Number(n) + " >= " + String::Number(m_normals.size()) + ')'); Error("Normal index out of range (" + String::Number(n) + " >= " + String::Number(m_normals.size()) + ')');
error = true; error = true;
break; break;
} }
else if (t >= 0 && static_cast<unsigned int>(t) >= m_texCoords.size()) else if (t != 0 && static_cast<std::size_t>(t) > m_texCoords.size())
{ {
Error("TexCoord index out of range (" + String::Number(t) + " >= " + String::Number(m_texCoords.size()) + ')'); Error("TexCoord index out of range (" + String::Number(t) + " >= " + String::Number(m_texCoords.size()) + ')');
error = true; error = true;
@ -243,17 +203,14 @@ namespace Nz
} }
if (!error) if (!error)
{ currentMesh->faces.push_back(std::move(face));
if (!currentMesh) else
currentMesh = GetMaterial(meshName, matName); currentMesh->vertices.resize(face.firstVertex); //< Remove vertices
currentMesh->push_back(std::move(face));
}
break; break;
} }
case 'm': case 'm': //< MTLLib
#if NAZARA_UTILITY_STRICT_RESOURCE_PARSING #if NAZARA_UTILITY_STRICT_RESOURCE_PARSING
if (m_currentLine.GetWord(0).ToLower() != "mtllib") if (m_currentLine.GetWord(0).ToLower() != "mtllib")
UnrecognizedLine(); UnrecognizedLine();
@ -262,8 +219,8 @@ namespace Nz
m_mtlLib = m_currentLine.SubString(m_currentLine.GetWordPosition(1)); m_mtlLib = m_currentLine.SubString(m_currentLine.GetWordPosition(1));
break; break;
case 'g': case 'g': //< Group (inside a mesh)
case 'o': case 'o': //< Object (defines a mesh)
{ {
if (m_currentLine.GetSize() <= 2 || m_currentLine[1] != ' ') if (m_currentLine.GetSize() <= 2 || m_currentLine[1] != ' ')
{ {
@ -283,12 +240,12 @@ namespace Nz
} }
meshName = objectName; meshName = objectName;
currentMesh = GetMaterial(meshName, matName); currentMesh = nullptr;
break; break;
} }
#if NAZARA_UTILITY_STRICT_RESOURCE_PARSING #if NAZARA_UTILITY_STRICT_RESOURCE_PARSING
case 's': case 's': //< Smooth
if (m_currentLine.GetSize() <= 2 || m_currentLine[1] == ' ') if (m_currentLine.GetSize() <= 2 || m_currentLine[1] == ' ')
{ {
String param = m_currentLine.SubString(2); String param = m_currentLine.SubString(2);
@ -298,15 +255,16 @@ namespace Nz
else else
UnrecognizedLine(); UnrecognizedLine();
break; break;
#endif #endif
case 'u': case 'u': //< Usemtl
#if NAZARA_UTILITY_STRICT_RESOURCE_PARSING #if NAZARA_UTILITY_STRICT_RESOURCE_PARSING
if (m_currentLine.GetWord(0) != "usemtl") if (m_currentLine.GetWord(0) != "usemtl")
UnrecognizedLine(); UnrecognizedLine();
#endif #endif
matName = m_currentLine.SubString(m_currentLine.GetWordPosition(1)); matName = m_currentLine.SubString(m_currentLine.GetWordPosition(1));
currentMesh = nullptr;
if (matName.IsEmpty()) if (matName.IsEmpty())
{ {
#if NAZARA_UTILITY_STRICT_RESOURCE_PARSING #if NAZARA_UTILITY_STRICT_RESOURCE_PARSING
@ -314,18 +272,16 @@ namespace Nz
#endif #endif
break; break;
} }
currentMesh = GetMaterial(meshName, matName);
break; break;
case 'v': case 'v': //< Position/Normal/Texcoords
{ {
String word = m_currentLine.GetWord(0).ToLower(); String word = m_currentLine.GetWord(0).ToLower();
if (word == 'v') if (word == 'v')
{ {
Vector4f vertex(Vector3f::Zero(), 1.f); Vector4f vertex(Vector3f::Zero(), 1.f);
unsigned int paramCount = std::sscanf(&m_currentLine[2], "%f %f %f %f", &vertex.x, &vertex.y, &vertex.z, &vertex.w); unsigned int paramCount = std::sscanf(&m_currentLine[2], "%f %f %f %f", &vertex.x, &vertex.y, &vertex.z, &vertex.w);
if (paramCount >= 3) if (paramCount >= 1)
m_positions.push_back(vertex); m_positions.push_back(vertex);
#if NAZARA_UTILITY_STRICT_RESOURCE_PARSING #if NAZARA_UTILITY_STRICT_RESOURCE_PARSING
else else
@ -371,26 +327,24 @@ namespace Nz
} }
std::unordered_map<String, unsigned int> materials; std::unordered_map<String, unsigned int> materials;
m_materials.resize(matIndex); m_materials.resize(matCount);
for (auto& meshIt : meshes) for (auto& meshPair : meshesByName)
{ {
for (auto& matIt : meshIt.second) for (auto& matPair : meshPair.second)
{ {
auto& faceVec = matIt.second.first; Mesh& mesh = matPair.second.first;
unsigned int index = matIt.second.second; unsigned int index = matPair.second.second;
if (!faceVec.empty()) if (!mesh.faces.empty())
{ {
Mesh mesh; mesh.name = meshPair.first;
mesh.faces = std::move(faceVec);
mesh.name = meshIt.first;
auto it = materials.find(matIt.first); auto it = materials.find(matPair.first);
if (it == materials.end()) if (it == materials.end())
{ {
mesh.material = index; mesh.material = index;
materials[matIt.first] = index; materials[matPair.first] = index;
m_materials[index] = matIt.first; m_materials[index] = matPair.first;
} }
else else
mesh.material = it->second; mesh.material = it->second;
@ -415,7 +369,7 @@ namespace Nz
{ {
do do
{ {
if (m_stream.EndOfStream()) if (m_currentStream->EndOfStream())
{ {
if (required) if (required)
Error("Incomplete OBJ file"); Error("Incomplete OBJ file");
@ -425,9 +379,8 @@ namespace Nz
m_lineCount++; m_lineCount++;
m_currentLine = m_stream.ReadLine(); m_currentLine = m_currentStream->ReadLine();
m_currentLine = m_currentLine.SubStringTo("#"); // On ignore les commentaires m_currentLine.Simplify(); // Simplify lines (convert multiple blanks into a single space and trims)
m_currentLine.Simplify(); // Pour un traitement plus simple
} }
while (m_currentLine.IsEmpty()); while (m_currentLine.IsEmpty());
} }
@ -436,24 +389,4 @@ namespace Nz
return true; return true;
} }
void OBJParser::Error(const String& message)
{
NazaraError(message + " at line #" + String::Number(m_lineCount));
}
void OBJParser::Warning(const String& message)
{
NazaraWarning(message + " at line #" + String::Number(m_lineCount));
}
void OBJParser::UnrecognizedLine(bool error)
{
String message = "Unrecognized \"" + m_currentLine + '"';
if (error)
Error(message);
else
Warning(message);
}
} }