1
0

Rewrote furnace recipe parser

* Fixes #110
This commit is contained in:
Tiger Wang 2014-06-21 20:33:23 +01:00
parent 35a4aa419c
commit a908f39dde
2 changed files with 185 additions and 97 deletions

View File

@ -5,7 +5,8 @@
#include "Item.h"
#include <fstream>
#include <sstream>
#define FURNACE_RECIPE_FILE "furnace.txt"
@ -53,129 +54,194 @@ void cFurnaceRecipe::ReloadRecipes(void)
ClearRecipes();
LOGD("Loading furnace recipes...");
std::ifstream f;
char a_File[] = "furnace.txt";
f.open(a_File, std::ios::in);
if (!f.good())
std::ifstream F(FURNACE_RECIPE_FILE, std::ios::in);
if (!F.good())
{
f.close();
LOG("Could not open the furnace recipes file \"%s\"", a_File);
F.close();
LOG("Could not open the furnace recipes file \"%s\"", FURNACE_RECIPE_FILE);
return;
}
// TODO: Replace this messy parse with a high-level-structured one (ReadLine / ProcessLine)
bool bSyntaxError = false;
while (f.good())
{
char c;
AString SyntaxError;
unsigned int Line = 0;
AString ParsingLine;
//////////////////////////////////////////////////////////////////////////
// comments
f >> c;
f.unget();
if( c == '#' )
ASSERT(ParsingLine.empty());
while (std::getline(F, ParsingLine))
{
while( f.good() && c != '\n' )
Line++;
ParsingLine.erase(std::remove_if(ParsingLine.begin(), ParsingLine.end(), isspace), ParsingLine.end()); // Remove whitespace
if (ParsingLine.empty())
{
f.get( c );
}
continue;
}
// Comments
if (ParsingLine[0] == '#')
{
continue;
}
//////////////////////////////////////////////////////////////////////////
// Line breaks
f.get( c );
while( f.good() && ( c == '\n' || c == '\r' ) ) { f.get( c ); }
if (f.eof())
if (ParsingLine[0] == '!') // Fuels start with a bang :)
{
int IItemID = 0, IItemCount = 0, IItemHealth = 0, IBurnTime = 0;
AString::size_type BeginPos = 1; // Begin at one after exclamation mark (bang)
if (
!ReadMandatoryNumber(BeginPos, ":", ParsingLine, SyntaxError, Line, IItemID) || // Read item ID
!ReadOptionalNumbers(BeginPos, ":", "=", ParsingLine, SyntaxError, Line, IItemCount, IItemHealth) || // Read item count (and optionally health)
!ReadMandatoryNumber(BeginPos, "0123456789", ParsingLine, SyntaxError, Line, IBurnTime, true) // Read item burn time - last value
)
{
break;
}
f.unget();
//////////////////////////////////////////////////////////////////////////
// Check for fuel
f >> c;
if( c == '!' ) // It's fuel :)
{
// Read item
int IItemID = 0, IItemCount = 0, IItemHealth = 0;
f >> IItemID;
f >> c; if( c != ':' ) { bSyntaxError = true; break; }
f >> IItemCount;
// Optional health
f >> c;
if( c != ':' )
f.unget();
else
{
f >> IItemHealth;
}
// Burn time
int BurnTime;
f >> c; if( c != '=' ) { bSyntaxError = true; break; }
f >> BurnTime;
// Add to fuel list
Fuel F;
F.In = new cItem((ENUM_ITEM_ID)IItemID, (char)IItemCount, (short)IItemHealth);
F.BurnTime = BurnTime;
F.BurnTime = IBurnTime;
m_pState->Fuel.push_back(F);
printf("%i %i %i %i\n", IItemID, IItemCount, IItemHealth, IBurnTime);
continue;
}
f.unget();
//////////////////////////////////////////////////////////////////////////
// Read items
int IItemID = 0, IItemCount = 0, IItemHealth = 0;
f >> IItemID;
f >> c; if( c != ':' ) { bSyntaxError = true; break; }
f >> IItemCount;
// Optional health
f >> c;
if( c != ':' )
f.unget();
else
else if (isdigit(ParsingLine[0])) // Recipes start with a numeral :)
{
f >> IItemHealth;
}
int CookTime;
f >> c; if( c != '@' ) { bSyntaxError = true; break; }
f >> CookTime;
int IItemID = 0, IItemCount = 0, IItemHealth = 0, IBurnTime = 0;
int OItemID = 0, OItemCount = 0, OItemHealth = 0;
f >> c; if( c != '=' ) { bSyntaxError = true; break; }
f >> OItemID;
f >> c; if( c != ':' ) { bSyntaxError = true; break; }
f >> OItemCount;
AString::size_type BeginPos = 0; // Begin at start of line
// Optional health
f >> c;
if( c != ':' )
f.unget();
else
if (
!ReadMandatoryNumber(BeginPos, ":", ParsingLine, SyntaxError, Line, IItemID) || // Read item ID
!ReadOptionalNumbers(BeginPos, ":", "@", ParsingLine, SyntaxError, Line, IItemCount, IItemHealth) || // Read item count (and optionally health)
!ReadMandatoryNumber(BeginPos, "=", ParsingLine, SyntaxError, Line, IBurnTime) || // Read item burn time
!ReadMandatoryNumber(BeginPos, ":", ParsingLine, SyntaxError, Line, OItemID) || // Read result ID
!ReadOptionalNumbers(BeginPos, ":", "012456789", ParsingLine, SyntaxError, Line, OItemCount, OItemHealth, true) // Read result count (and optionally health) - last value
)
{
f >> OItemHealth;
break;
}
// Add to recipe list
Recipe R;
R.In = new cItem((ENUM_ITEM_ID)IItemID, (char)IItemCount, (short)IItemHealth);
R.Out = new cItem((ENUM_ITEM_ID)OItemID, (char)OItemCount, (short)OItemHealth);
R.CookTime = CookTime;
R.CookTime = IBurnTime;
m_pState->Recipes.push_back(R);
}
if (bSyntaxError)
{
LOGERROR("ERROR: FurnaceRecipe, syntax error" );
}
F.close();
if (!SyntaxError.empty())
{
LOGWARN("Error parsing furnace recipes at %s", SyntaxError.c_str());
}
else
{
LOG("Loaded " SIZE_T_FMT " furnace recipes and " SIZE_T_FMT " fuels", m_pState->Recipes.size(), m_pState->Fuel.size());
}
}
void cFurnaceRecipe::SetParseError(AString & a_ErrorString, unsigned int a_Line, size_t a_Position, const AString & a_CharactersMissing)
{
Printf(a_ErrorString, "line %i pos %i: missing '%s'", a_Line, a_Position, a_CharactersMissing.c_str());
}
bool cFurnaceRecipe::ReadMandatoryNumber(AString::size_type & a_Begin, const AString & a_Delimiter, const AString & a_Text, AString & a_ErrorString, unsigned int a_Line, int & a_Value, bool a_IsLastValue)
{
AString::size_type End;
if (a_IsLastValue)
{
End = a_Text.find_first_not_of(a_Delimiter, a_Begin);
}
else
{
End = a_Text.find_first_of(a_Delimiter, a_Begin);
if (End == AString::npos)
{
SetParseError(a_ErrorString, a_Line, a_Begin, a_Delimiter);
return false;
}
}
// stoi won't throw an exception if the string is alphanumeric, we should check for this
if (!DoesStringContainOnlyNumbers(a_Text.substr(a_Begin, End - a_Begin)))
{
SetParseError(a_ErrorString, a_Line, a_Begin, "number");
return false;
}
a_Value = std::stoi(a_Text.substr(a_Begin, End - a_Begin));
a_Begin = End + 1; // Jump over delimiter
return true;
}
bool cFurnaceRecipe::ReadOptionalNumbers(AString::size_type & a_Begin, const AString & a_DelimiterOne, const AString & a_DelimiterTwo, const AString & a_Text, AString & a_ErrorString, unsigned int a_Line, int & a_ValueOne, int & a_ValueTwo, bool a_IsLastValue)
{
unsigned int End, Begin = a_Begin;
End = a_Text.find_first_of(a_DelimiterOne, Begin);
if (End != AString::npos)
{
if (DoesStringContainOnlyNumbers(a_Text.substr(Begin, End - Begin)))
{
a_ValueOne = std::stoi(a_Text.substr(Begin, End - Begin));
Begin = End + 1;
if (a_IsLastValue)
{
End = a_Text.find_first_not_of(a_DelimiterTwo, Begin);
}
else
{
End = a_Text.find_first_of(a_DelimiterTwo, Begin);
if (End == AString::npos)
{
SetParseError(a_ErrorString, a_Line, Begin, a_DelimiterTwo);
return false;
}
}
// stoi won't throw an exception if the string is alphanumeric, we should check for this
if (!DoesStringContainOnlyNumbers(a_Text.substr(Begin, End - Begin)))
{
SetParseError(a_ErrorString, a_Line, Begin, "number");
return false;
}
a_ValueTwo = std::stoi(a_Text.substr(Begin, End - Begin));
a_Begin = End + 1; // Jump over delimiter
return true;
}
else
{
return ReadMandatoryNumber(a_Begin, a_DelimiterTwo, a_Text, a_ErrorString, a_Line, a_ValueOne, a_IsLastValue);
}
}
return ReadMandatoryNumber(a_Begin, a_DelimiterTwo, a_Text, a_ErrorString, a_Line, a_ValueOne, a_IsLastValue);
}
bool cFurnaceRecipe::DoesStringContainOnlyNumbers(const AString & a_String)
{
return std::all_of(a_String.begin(), a_String.end(), isdigit);
}

View File

@ -41,6 +41,28 @@ public:
private:
void ClearRecipes(void);
/** PrintFs the line, position, and error into an error string */
inline static void SetParseError(AString & a_ErrorString, unsigned int a_Line, size_t a_Position, const AString & a_CharactersMissing);
/** Reads a number from a string given, starting at a given position and ending at a delimiter given
Updates beginning position to the delimiter found + 1, and updates the value to the one read
If it encounters a substring that is not fully numeric, it will call SetParseError() and return false; the caller should abort processing
Otherwise, the function will return true
*/
static bool ReadMandatoryNumber(AString::size_type & a_Begin, const AString & a_Delimiter, const AString & a_Text, AString & a_ErrorString, unsigned int a_Line, int & a_Value, bool a_IsLastValue = false);
/** Reads two numbers from a string given, starting at a given position and ending at the first delimiter given, then again (with an updated position) until the second delimiter given
Updates beginning position to the second delimiter found + 1, and updates the values to the ones read
If it encounters a substring that is not fully numeric whilst reading the second value, it will call SetParseError() and return false; the caller should abort processing
If this happens whilst reading the first value, it will call ReadMandatoryNumber() with the appropriate position, as this may legitimately occur with the optional value and AString::find_first_of finding the incorrect delimiter. It will return the result of ReadMandatoryNumber()
True will be returned definitively for an optional value that is valid
*/
static bool ReadOptionalNumbers(AString::size_type & a_Begin, const AString & a_DelimiterOne, const AString & a_DelimiterTwo, const AString & a_Text, AString & a_ErrorString, unsigned int a_Line, int & a_ValueOne, int & a_ValueTwo, bool a_IsLastValue = false);
/** Uses std::all_of to determine if a string contains only digits */
inline static bool DoesStringContainOnlyNumbers(const AString & a_String);
struct sFurnaceRecipeState;
sFurnaceRecipeState * m_pState;
};