diff --git a/src/feature.cpp b/src/feature.cpp index b7fe2f2dd..2eb2850f1 100644 --- a/src/feature.cpp +++ b/src/feature.cpp @@ -72,13 +72,7 @@ FEATURE_STATS* oilResFeature = NULL; //specifies how far round (in tiles) a constructor droid sound look for more wreckage #define WRECK_SEARCH 3 -struct featureTypeMap -{ - const char *typeStr; - FEATURE_TYPE type; -}; - -static const struct featureTypeMap map[] = +static const StringToEnum mapUnsorted_FEATURE_TYPE[] = { { "PROPULSION_TYPE_HOVER WRECK", FEAT_HOVER }, { "TANK WRECK", FEAT_TANK }, @@ -93,6 +87,7 @@ static const struct featureTypeMap map[] = { "TREE", FEAT_TREE }, { "SKYSCRAPER", FEAT_SKYSCRAPER } }; +static const StringToEnumMap map_FEATURE_TYPE(mapUnsorted_FEATURE_TYPE, ARRAY_SIZE(mapUnsorted_FEATURE_TYPE)); void featureInitVars(void) @@ -102,112 +97,54 @@ void featureInitVars(void) oilResFeature = NULL; } -static void featureType(FEATURE_STATS* psFeature, const char *pType) +FEATURE_STATS::FEATURE_STATS(LineView line) + : BASE_STATS(REF_FEATURE_START + line.line(), line.s(0)) + , subType(line.e(7, map_FEATURE_TYPE)) + , psImd(line.imdShape(6)) + , baseWidth(line.u16(1)) + , baseBreadth(line.u16(2)) + , tileDraw(line.b(8)) + , allowLOS(line.b(9)) + , visibleAtStart(line.b(10)) + , damageable(line.b(3)) + , body(line.u32(5)) + , armourValue(line.u32(4)) { - unsigned int i; - - for (i = 0; i < ARRAY_SIZE(map); i++) + if (damageable && body == 0) { - if (strcmp(pType, map[i].typeStr) == 0) - { - psFeature->subType = map[i].type; - return; - } + debug(LOG_ERROR, "The feature \"%s\", ref %d is damageable, but has no body points! The files need to be updated / fixed. Assigning 1 body point to feature.", pName, ref); + body = 1; } - - ASSERT(false, "featureType: Unknown feature type"); } /* Load the feature stats */ BOOL loadFeatureStats(const char *pFeatureData, UDWORD bufferSize) { - FEATURE_STATS *psFeature; - unsigned int i; - char featureName[MAX_STR_LENGTH], GfxFile[MAX_STR_LENGTH], - type[MAX_STR_LENGTH]; - - numFeatureStats = numCR(pFeatureData, bufferSize); - // Skip descriptive header if (strncmp(pFeatureData,"Feature ",8)==0) { pFeatureData = strchr(pFeatureData,'\n') + 1; - numFeatureStats--; - } - - asFeatureStats = (FEATURE_STATS*)malloc(sizeof(FEATURE_STATS) * numFeatureStats); - - if (asFeatureStats == NULL) - { - debug( LOG_FATAL, "Feature Stats - Out of memory" ); - abort(); - return false; } - psFeature = asFeatureStats; + TableView table(pFeatureData, bufferSize); - for (i = 0; i < numFeatureStats; i++) + asFeatureStats = new FEATURE_STATS[table.size()]; + numFeatureStats = table.size(); + + for (unsigned i = 0; i < table.size(); ++i) { - UDWORD Width, Breadth; - int damageable = 0, tileDraw = 0, allowLOS = 0, visibleAtStart = 0; - - memset(psFeature, 0, sizeof(FEATURE_STATS)); - - featureName[0] = '\0'; - GfxFile[0] = '\0'; - type[0] = '\0'; - - //read the data into the storage - the data is delimeted using comma's - sscanf(pFeatureData, "%[^','],%d,%d,%d,%d,%d,%[^','],%[^','],%d,%d,%d", - featureName, &Width, &Breadth, - &damageable, &psFeature->armourValue, &psFeature->body, - GfxFile, type, &tileDraw, &allowLOS, - &visibleAtStart); - - psFeature->damageable = damageable; - psFeature->tileDraw = tileDraw; - psFeature->allowLOS = allowLOS; - psFeature->visibleAtStart = visibleAtStart; - - // These are now only 16 bits wide - so we need to copy them - psFeature->baseWidth = Width; - psFeature->baseBreadth = Breadth; - - psFeature->pName = allocateName(featureName); - if (!psFeature->pName) + asFeatureStats[i] = FEATURE_STATS(LineView(table, i)); + if (table.isError()) { + debug(LOG_ERROR, "%s", table.getError().toUtf8().constData()); return false; } - if (psFeature->damageable && psFeature->body == 0) - { - debug(LOG_ERROR, "The feature %s, ref %d, is damageable, but has no body points! The files need to be updated / fixed. " \ - "Assigning 1 body point to feature.", psFeature->pName, psFeature->ref); - psFeature->body = 1; - } - //determine the feature type - featureType(psFeature, type); - //and the oil resource - assumes only one! - if (psFeature->subType == FEAT_OIL_RESOURCE) + if (asFeatureStats[i].subType == FEAT_OIL_RESOURCE) { - oilResFeature = psFeature; + oilResFeature = &asFeatureStats[i]; } - - //get the IMD for the feature - psFeature->psImd = (iIMDShape *) resGetData("IMD", GfxFile); - if (psFeature->psImd == NULL) - { - debug( LOG_ERROR, "Cannot find the feature PIE for record %s", getName( psFeature->pName ) ); - return false; - } - - psFeature->ref = REF_FEATURE_START + i; - - //increment the pointer to the start of the next record - pFeatureData = strchr(pFeatureData,'\n') + 1; - //increment the list to the start of the next storage block - psFeature++; } return true; @@ -216,11 +153,13 @@ BOOL loadFeatureStats(const char *pFeatureData, UDWORD bufferSize) /* Release the feature stats memory */ void featureStatsShutDown(void) { - if(numFeatureStats) + for (unsigned i = 0; i < numFeatureStats; ++i) { - free(asFeatureStats); - asFeatureStats = NULL; + free(asFeatureStats[i].pName); } + delete[] asFeatureStats; + asFeatureStats = NULL; + numFeatureStats = 0; } /** Deals with damage to a feature diff --git a/src/featuredef.h b/src/featuredef.h index d29508d2a..5716d8197 100644 --- a/src/featuredef.h +++ b/src/featuredef.h @@ -60,6 +60,9 @@ typedef enum _feature_type /* Stats for a feature */ struct FEATURE_STATS : public BASE_STATS { + FEATURE_STATS() {} + FEATURE_STATS(LineView line); + FEATURE_TYPE subType; ///< type of feature iIMDShape* psImd; ///< Graphic for the feature diff --git a/src/stats.cpp b/src/stats.cpp index ed5aff76a..05232e64e 100644 --- a/src/stats.cpp +++ b/src/stats.cpp @@ -24,6 +24,7 @@ * */ #include +#include #include "lib/framework/frame.h" #include "lib/framework/strres.h" @@ -123,6 +124,228 @@ static void updateMaxECMStats(UWORD maxValue); static void updateMaxBodyStats(UWORD maxBody, UWORD maxPower, UWORD maxArmour); static void updateMaxConstStats(UWORD maxValue); + +BASE_STATS::BASE_STATS(unsigned ref, std::string const &str) + : ref(ref) + , pName(allocateName(str.c_str())) +{} + + +TableView::TableView(char const *buffer, unsigned size) + : buffer(buffer) +{ + size = std::min(size, UINT32_MAX - 1); // Shouldn't be a problem... + + char const *bufferEnd = buffer + size; + bufferEnd = std::find(buffer, bufferEnd, '\0'); // Truncate buffer at first null character, if any. + + // Split into lines. + char const *lineNext = buffer; + while (lineNext != bufferEnd) + { + char const *lineBegin = lineNext; + char const *lineEnd = std::find(lineBegin, bufferEnd, '\n'); + lineNext = lineEnd + (lineEnd != bufferEnd); + + // Remove stuff that isn't data. + lineEnd = std::find(lineBegin, lineEnd, '#'); // Remove comments, if present. + while (lineBegin != lineEnd && uint8_t(*(lineEnd - 1)) <= ' ') + { + --lineEnd; // Remove trailing whitespace, if present. + } + while (lineBegin != lineEnd && uint8_t(*lineBegin) <= ' ') + { + ++lineBegin; // Remove leading whitespace, if present. + } + if (lineBegin == lineEnd) + { + continue; // Remove empty lines. + } + + // Split into cells. + size_t firstCell = cells.size(); + char const *cellNext = lineBegin; + while (cellNext != lineEnd) + { + char const *cellBegin = cellNext; + char const *cellEnd = std::find(cellBegin, lineEnd, ','); + cellNext = cellEnd + (cellEnd != lineEnd); + + cells.push_back(cellBegin - buffer); + } + cells.push_back(lineEnd - buffer + 1); + lines.push_back(std::make_pair(firstCell, cells.size() - firstCell)); + } +} + +void LineView::setError(unsigned index, char const *error) +{ + if (!table.parseError.isEmpty()) + { + return; // Already have an error. + } + + if (index < numCells) + { + char const *cellBegin = table.buffer + cells[index]; + char const *cellEnd = table.buffer + (cells[index + 1] - 1); + + char cellDesc[150]; + ssprintf(cellDesc, "Line %u, column %u \"%*s\": ", lineNumber, index, std::min(100, cellEnd - cellBegin), cellBegin); + table.parseError = QString::fromUtf8((std::string(cellDesc) + error).c_str()); + } + else + { + char cellDesc[50]; + ssprintf(cellDesc, "Line %u, column %u: ", lineNumber, index); + table.parseError = QString::fromUtf8((std::string(cellDesc) + error).c_str()); + } +} + +bool LineView::checkRange(unsigned index) +{ + if (index < numCells) + { + return true; + } + + setError(index, "Not enough cells."); + return false; +} + +int64_t LineView::i(unsigned index, int min, int max) +{ + int errorReturn = std::min(std::max(0, min), max); // On error, return 0 if possible. + + if (!checkRange(index)) + { + return errorReturn; + } + + char const *cellBegin = table.buffer + cells[index]; + char const *cellEnd = table.buffer + (cells[index + 1] - 1); + if (cellBegin != cellEnd) + { + bool negative = false; + switch (*cellBegin++) + { + case '-': + negative = true; + break; + case '+': + break; + default: + --cellBegin; + break; + } + int64_t absolutePart = 0; + while (cellBegin != cellEnd && absolutePart < int64_t(1000000000)*1000000000) + { + unsigned digit = *cellBegin - '0'; + if (digit > 9) + { + break; + } + absolutePart = absolutePart*10 + digit; + ++cellBegin; + } + if (cellBegin == cellEnd) + { + int64_t result = negative? -absolutePart : absolutePart; + if (result >= min && result <= max) + { + return result; // Maybe should just have copied the string to null-terminate it, and used scanf... + } + setError(index, "Integer out of range."); + return errorReturn; + } + } + + setError(index, "Expected integer."); + return errorReturn; +} + +float LineView::f(unsigned index, float min, float max) +{ + float errorReturn = std::min(std::max(0.f, min), max); // On error, return 0 if possible. + + std::string const &str = s(index); + if (!str.empty()) + { + int parsed = 0; + float result; + sscanf(str.c_str(), "%f%n", &result, &parsed); + if ((unsigned)parsed == str.size()) + { + if (result >= min && result <= max) + { + return result; + } + setError(index, "Float out of range."); + return errorReturn; + } + } + + setError(index, "Expected float."); + return errorReturn; +} + +std::string const &LineView::s(unsigned index) +{ + if (!checkRange(index)) + { + table.returnString.clear(); + return table.returnString; + } + + char const *cellBegin = table.buffer + cells[index]; + char const *cellEnd = table.buffer + (cells[index + 1] - 1); + table.returnString.assign(cellBegin, cellEnd); + return table.returnString; +} + +iIMDShape *LineView::imdShape(unsigned int index) +{ + std::string const &str = s(index); + iIMDShape *result = (iIMDShape *)resGetData("IMD", str.c_str()); + if (result == NULL) + { + setError(index, "Expected PIE shape."); + } + return result; +} + +static inline bool stringToEnumFindFunction(std::pair const &a, char const *b) +{ + return strcmp(a.first, b) < 0; +} + +unsigned LineView::eu(unsigned int index, std::vector > const &map) +{ + typedef std::vector > M; + + std::string const &str = s(index); + M::const_iterator i = std::lower_bound(map.begin(), map.end(), str.c_str(), stringToEnumFindFunction); + if (i != map.end() && strcmp(i->first, str.c_str()) == 0) + { + return i->second; + } + + // Didn't find it, give error and return 0. + if (table.parseError.isEmpty()) + { + std::string values = "Expected one of"; + for (i = map.begin(); i != map.end(); ++i) + { + values += std::string(" \"") + i->first + '"'; + } + values = values + '.'; + setError(index, "Expected one of"); + } + return 0; +} + + /******************************************************************************* * Generic stats macros/functions *******************************************************************************/ diff --git a/src/stats.h b/src/stats.h index c04ef3348..74faa5067 100644 --- a/src/stats.h +++ b/src/stats.h @@ -23,6 +23,8 @@ #ifndef __INCLUDED_SRC_STATS_H__ #define __INCLUDED_SRC_STATS_H__ +#include + #include "objectdef.h" /************************************************************************************** diff --git a/src/statsdef.h b/src/statsdef.h index e6b4c0720..2924c3c44 100644 --- a/src/statsdef.h +++ b/src/statsdef.h @@ -26,6 +26,84 @@ #include "lib/ivis_opengl/ivisdef.h" +#include +#include +#include "lib/framework/utf.h" // For QString. + +static inline bool stringToEnumSortFunction(std::pair const &a, std::pair const &b) { return strcmp(a.first, b.first) < 0; } + +template +struct StringToEnum +{ + operator std::pair() const { return std::make_pair(string, value); } + + char const * string; + Enum value; +}; + +template +struct StringToEnumMap : public std::vector > +{ + typedef std::vector > V; + + StringToEnumMap(StringToEnum const entries[], unsigned numEntries) : V(entries, entries + numEntries) { std::sort(V::begin(), V::end(), stringToEnumSortFunction); } +}; + +/// Read-only view of file data in "A,1,2\nB,3,4" format as a 2D array-like object. Note — does not copy the file data. +class TableView +{ +public: + TableView(char const *buffer, unsigned size); + + bool isError() const { return parseError.isEmpty(); } + QString getError() const { return parseError; } + + unsigned size() const { return lines.size(); } + +private: + friend class LineView; + char const * buffer; + std::vector cells; + std::vector > lines; + QString parseError; + std::string returnString; +}; + +/// Read-only view of a line of file data in "A,1,2" format as an array-like object. Note — does not copy the file data. +class LineView +{ +public: + LineView(class TableView &table, unsigned lineNumber) : table(table), cells(&table.cells[table.lines.at(lineNumber).first]), numCells(table.lines.at(lineNumber).second), lineNumber(lineNumber) {} ///< This LineView is only valid for the lifetime of the TableView. + + void setError(unsigned index, char const *error); ///< Only the first error is saved. + + unsigned size() const { return numCells; } + unsigned line() const { return lineNumber; } + + bool b(unsigned index) { return i(index, 0, 1); } + int64_t i(unsigned index, int min, int max); + uint32_t i8(unsigned index) { return i(index, INT8_MIN, INT8_MAX); } + uint32_t u8(unsigned index) { return i(index, 0, UINT8_MAX); } + uint32_t i16(unsigned index) { return i(index, INT16_MIN, INT16_MAX); } + uint32_t u16(unsigned index) { return i(index, 0, UINT16_MAX); } + uint32_t i32(unsigned index) { return i(index, INT32_MIN, INT32_MAX); } + uint32_t u32(unsigned index) { return i(index, 0, UINT32_MAX); } + float f(unsigned index, float min = -1.e30f, float max = 1.e30f); + std::string const &s(unsigned index); + template + Enum e(unsigned index, StringToEnumMap const &map) { return (Enum)eu(index, map); } + + iIMDShape *imdShape(unsigned index); + +private: + unsigned eu(unsigned index, std::vector > const &map); + bool checkRange(unsigned index); + class TableView & table; + unsigned const * cells; + unsigned numCells; + unsigned lineNumber; +}; + /* if any types are added BEFORE 'COMP_BODY' - then Save/Load Game will have to be altered since it loops through the components from 1->MAX_COMP @@ -206,6 +284,12 @@ typedef enum TRAVEL_MEDIUM /* Stats common to all stats structs */ struct BASE_STATS { + BASE_STATS() : ref(0), pName(NULL) {} ///< Only initialised here when using new/delete! TODO Use new/delete only, not malloc()/free(). + BASE_STATS(unsigned ref, std::string const &str); ///< Only initialised here when using new/delete! TODO Use new/delete only, not malloc()/free(). TODO Then pName could be a QString... + //Gah, too soon to add destructors to BASE_STATS, thanks to local temporaries that are copied with memcpy()... --- virtual ~BASE_STATS() { free(pName); } ///< pName is only freed here when using new/delete! TODO Use new/delete only, not malloc()/free(). + //So this one isn't needed for now, maybe not ever. --- BASE_STATS(BASE_STATS const &stats) : ref(stats.ref), pName(strdup(stats.pName)) {} // TODO Not needed when pName is a QString... + //So this one isn't needed for now, maybe not ever. --- BASE_STATS const &operator =(BASE_STATS const &stats) { ref = stats.ref; free(pName); pName = strdup(stats.pName); return *this; } // TODO Not needed when pName is a QString... + UDWORD ref; /**< Unique ID of the item */ char *pName; /**< pointer to the text id name (i.e. short language-independant name) */ };