diff --git a/.gitignore b/.gitignore index c6953c9..e66a5f2 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,6 @@ bin/ # ignore vscode .vscode/ + +# ignore cmake tools +build/ diff --git a/CMakeLists.txt b/CMakeLists.txt index ec0211d..e375e6f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,7 +12,7 @@ file(GLOB headers "lib/*.hpp") file(GLOB sources "lib/*.cpp") add_library(grandmaze SHARED ${sources} ${headers}) -target_link_libraries(grandmaze ncurses) +target_link_libraries(grandmaze ncurses sqlite3) install(TARGETS grandmaze LIBRARY DESTINATION "lib") diff --git a/lib/colour.hpp b/lib/colour.hpp index 2685ec7..f7b68ea 100644 --- a/lib/colour.hpp +++ b/lib/colour.hpp @@ -3,6 +3,7 @@ namespace GrandMaze { + // All colours enum Colour { None, @@ -27,6 +28,7 @@ namespace GrandMaze WhiteInvert = Invert, }; + // Initialise colours void initColour(void); }; diff --git a/lib/exception.cpp b/lib/exception.cpp new file mode 100644 index 0000000..42dfa53 --- /dev/null +++ b/lib/exception.cpp @@ -0,0 +1,16 @@ +#include + +#include "exception.hpp" + +using namespace GrandMaze; + +DatabaseError::DatabaseError(int errorCode) : errorCode(errorCode) +{ + +} + +const char* DatabaseError::what() const noexcept +{ + // get sqlite3 error from error code + return sqlite3_errstr(errorCode); +} \ No newline at end of file diff --git a/lib/exception.hpp b/lib/exception.hpp new file mode 100644 index 0000000..0f18596 --- /dev/null +++ b/lib/exception.hpp @@ -0,0 +1,21 @@ +#ifndef EXCEPTION_H +#define EXCEPTION_H + +#include + +namespace GrandMaze +{ + class DatabaseError : public std::exception + { + private: + int errorCode; + + public: + // Construct a DatabaseError with a SQLite error code + DatabaseError(int errorCode); + + const char* what() const noexcept; + }; +} + +#endif diff --git a/lib/form.cpp b/lib/form.cpp index c03418d..4b2a1c4 100644 --- a/lib/form.cpp +++ b/lib/form.cpp @@ -16,10 +16,13 @@ Form::Form(WINDOW *win, std::string title) void Form::draw(void) { wclear(win); + // get max column int maxcol = getmaxx(win); box(win, '|', '-'); + // insert title mvwaddstr(win, 1, (maxcol/2)-(title.size()/2), title.c_str()); for (int i = 1; i < maxcol-1; i++) + // add title underline mvwaddch(win, 2, i, '-'); drawFields(); @@ -40,6 +43,7 @@ void Form::drawField(Field *field) int x = field->x + 1; int y = field->y + 3; + // if showName, display the name next to the field if (field->showName) { boxSize -= field->name.size() + 1; @@ -47,8 +51,9 @@ void Form::drawField(Field *field) waddch(win, ' '); } - x = getcurx(win); + x = getcurx(win); + // draw field box for (int i = 0; i < boxSize; i++) { char mid = i == 0 || i == boxSize-1 ? '|' : ' '; @@ -61,12 +66,15 @@ void Form::drawField(Field *field) } } + // draw text if not empty if (field->value.size() > 0) { int textSize = boxSize - 3; char *textToDiplay = new char[textSize+1]; + // if text is bigger than the field if (textSize < field->value.size()) { + // tail text so that the it fits in the field field->value.copy( textToDiplay, textSize, @@ -76,11 +84,16 @@ void Form::drawField(Field *field) } else { + // the whole string fits, so copy direct field->value.copy(textToDiplay, field->value.size()); textToDiplay[field->value.size()] = '\0'; } + // insert tailed text mvwaddstr(win, y+1, x+1, textToDiplay); - + // free memory + delete textToDiplay; + + // if field being draw is the current focused one, then add cursor if (*currentField == field) waddch(win, '_' | A_BLINK); } @@ -98,34 +111,45 @@ void Form::addField(Field *field) void Form::show(FormCallback callback) { + // set current field as the first field currentField = fields.begin(); draw(); do { + // get user input int c = wgetch(win); + // pass user input to form callback FormOp op = callback(c); + + // process returned operation if (op == NoOp) { + // do nothing continue; } else if (op == Push) { + // add last inputted char to value of the current field (*currentField)->value.push_back(c); drawField(*currentField); } else if (op == Pop) { + // if there is a value to delete from if ((*currentField)->value.size() > 0) { + // delete last char from value (*currentField)->value.pop_back(); drawField(*currentField); } } else if (op == NextField) { + // if the next field in the iterator isn't the end if (currentField+1 != fields.end()) { + // increment to next field currentField++; drawField(*(currentField-1)); drawField(*currentField); @@ -133,8 +157,10 @@ void Form::show(FormCallback callback) } else if (op == PrevField) { + // if we aren't at the first field if (currentField != fields.begin()) { + // decrement to previous field currentField--; drawField(*(currentField+1)); drawField(*currentField); @@ -142,10 +168,12 @@ void Form::show(FormCallback callback) } else if (op == SubmitForm) { + // return value to calling function break; } else if (op == CancelForm) { + // erase all values and return to calling function for (auto field : fields) { field->value.erase(); diff --git a/lib/form.hpp b/lib/form.hpp index 768c755..c4089bc 100644 --- a/lib/form.hpp +++ b/lib/form.hpp @@ -10,6 +10,7 @@ namespace GrandMaze { + // Form operations enum FormOp { NoOp, @@ -21,6 +22,8 @@ namespace GrandMaze CancelForm, }; + // FormCallback is a callback function that takes an int + // and returns a FormOp typedef std::function FormCallback; class Field @@ -50,14 +53,19 @@ namespace GrandMaze std::string title; std::vector::iterator currentField; + // Draw the form void draw(void); + // Draw fields in the form void drawFields(void); + // Draw a single field in the form void drawField(Field *field); public: Form(WINDOW *win, std::string title); + // Add a new field to the form void addField(Field *field); + // Display and get input from the form void show(FormCallback callback); }; }; diff --git a/lib/keys.hpp b/lib/keys.hpp index 5279216..f1db18c 100644 --- a/lib/keys.hpp +++ b/lib/keys.hpp @@ -5,6 +5,7 @@ namespace GrandMaze { + // User-friendly key names enum Keys { Enter = 10, diff --git a/lib/map.cpp b/lib/map.cpp index 5ec44b1..4d3c30e 100644 --- a/lib/map.cpp +++ b/lib/map.cpp @@ -29,10 +29,12 @@ void Map::addRooms(std::initializer_list rooms) void Map::delRoom(Room *room) { + // find room auto elem = std::find(rooms.begin(), rooms.end(), room); if (elem == rooms.end()) return; + // delete room rooms.erase(elem); } @@ -46,6 +48,7 @@ void Map::delRooms(std::initializer_list rooms) void Map::draw(WINDOW *win) { + // draw all rooms in map for (Room *room : rooms) { room->draw(win); diff --git a/lib/mapfile.cpp b/lib/mapfile.cpp index 1a6fc25..0532034 100644 --- a/lib/mapfile.cpp +++ b/lib/mapfile.cpp @@ -1,19 +1,38 @@ #include #include #include +#include #include #include "map.hpp" #include "mapfile.hpp" #include "colour.hpp" +#include "exception.hpp" using namespace GrandMaze; +// ad hoc; copied from and modified: +// https://stackoverflow.com/questions/7781898/get-an-istream-from-a-char +struct membuf : std::streambuf +{ + membuf(char* buf, size_t size) + { + this->setg(buf, buf, buf+size); + } +}; MapWriter::MapWriter(Map *map) : map(map) { } +size_t MapWriter::getSize(void) +{ + return ( + map->getRooms().size() * 9 + + MAGIC_NUMBER_v2.size() + ); +} + void MapWriter::writeRoom(std::ostream *out, Room *room) { *out << (uint8_t)room->getType(); @@ -174,3 +193,192 @@ void FileMapWriter::write(std::string filename) out->close(); delete out; } + +DbMapReader::DbMapReader(sqlite3 *db) : db(db) +{ + +} + +Map* DbMapReader::load(std::string table, std::string column, int rowID) +{ + int err, size; + char *buffer; + sqlite3_blob *blob; + + // open blob + err = sqlite3_blob_open( + db, + "main", + table.c_str(), + column.c_str(), + rowID, + 0, + &blob + ); + if (err != SQLITE_OK) + throw DatabaseError(err); + + // get size of blob + size = sqlite3_blob_bytes(blob); + buffer = new char[size]; + // read blob into buffer + err = sqlite3_blob_read(blob, buffer, size, 0); + if (err != SQLITE_OK) + { + delete buffer; + throw DatabaseError(err); + } + + Map *m; + // store buffer in membuf for use with istream + auto cbuf = new membuf(buffer, size); + auto is = new std::istream(cbuf); + // load map using istream + m = MapReader::load(is); + + // free up memory + delete buffer; + delete cbuf; + delete is; + + // close blob + err = sqlite3_blob_close(blob); + if (err != SQLITE_OK) + { + delete m; + throw DatabaseError(err); + } + + return m; +} + +DbMapWriter::DbMapWriter(Map *map, sqlite3 *db) : MapWriter(map), db(db) +{ + +} + +bool DbMapWriter::isValidSQLInput(std::string input) +{ + // help prevent sql injection + const std::string valid = + "abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "0123456789" + "-_"; + for (char c : input) + { + // if character is not in valid character + if (valid.find(c) == valid.npos) + return false; + } + return true; +} + +void DbMapWriter::write(std::string table, std::string column) +{ + if (!isValidSQLInput(table)) + throw std::invalid_argument("table isn't a valid SQL-safe string"); + else if (!isValidSQLInput(column)) + throw std::invalid_argument("column isn't a valid SQL-safe string"); + + int err, rowid; + size_t size; + char *buffer; + sqlite3_stmt *stmt; + std::string sql = + "INSERT INTO `" + table + "`" + " (`" + column + "`)" + "VALUES (?);"; + + // prep statement + err = sqlite3_prepare_v2(db, sql.c_str(), sql.size(), &stmt, NULL); + if (err != SQLITE_OK) + throw DatabaseError(err); + + // write to buffer step + size = MapWriter::getSize(); + buffer = new char[size]; + auto cbuf = new membuf(buffer, size); + auto os = new std::ostream(cbuf); + // write via ostream into buffer + MapWriter::write(os); + + for (int i = 0; i < size; i++) + { + std::cout << (int)buffer[i] << std::endl; + } + + // bind the blob + err = sqlite3_bind_blob(stmt, 1, buffer, size, SQLITE_STATIC); + if (err != SQLITE_OK) + throw DatabaseError(err); + + // advance the statement + err = sqlite3_step(stmt); + + // clean up + delete buffer; + delete cbuf; + delete os; + + // check for error + if (err != SQLITE_DONE) + throw DatabaseError(err); + + // destroy statement + err = sqlite3_finalize(stmt); + if (err != SQLITE_OK) + throw DatabaseError(err); + + // get last rowid + rowid = sqlite3_last_insert_rowid(db); + + // write using update method + // write(table, column, rowid); +} + +void DbMapWriter::write(std::string table, std::string column, long rowid) +{ + int err; + size_t size; + char *buffer; + sqlite3_blob *blob; + + // open the blob + err = sqlite3_blob_open( + db, + "main", + table.c_str(), + column.c_str(), + rowid, + 0, + &blob + ); + if (err != SQLITE_OK) + throw DatabaseError(err); + + // get the size in bytes of the map file being produced + size = MapWriter::getSize(); + buffer = new char[size]; + auto cbuf = new membuf(buffer, size); + auto os = new std::ostream(cbuf); + // write via ostream into buffer + MapWriter::write(os); + + // write buffer to blob + err = sqlite3_blob_write(blob, buffer, size, 0); + + // clean up + delete buffer; + delete cbuf; + delete os; + + // delayed error check after clean up + if (err != SQLITE_OK) + throw DatabaseError(err); + + // close blob + err = sqlite3_blob_close(blob); + if (err != SQLITE_OK) + throw DatabaseError(err); +} diff --git a/lib/mapfile.hpp b/lib/mapfile.hpp index 6aa2cc8..67402d7 100644 --- a/lib/mapfile.hpp +++ b/lib/mapfile.hpp @@ -4,6 +4,8 @@ #include #include +#include + #include "map.hpp" #include "room.hpp" @@ -17,15 +19,16 @@ namespace GrandMaze protected: Map *map; - // void writeVint(std::ofstream out, uint64_t value); - - // write the room to the output stream out + // Write the room to the output stream out void virtual writeRoom(std::ostream *out, Room *room); + // Get the output size of the map + size_t getSize(void); + public: MapWriter(Map *map); - // write the map to a given output stream + // Write the map to a given output stream void virtual write(std::ostream *stream); }; @@ -62,6 +65,36 @@ namespace GrandMaze // load from a file at location specified by filename Map* load(std::string filename); }; + + class DbMapReader : public MapReader + { + protected: + sqlite3 *db; + + public: + DbMapReader(sqlite3 *db); + + // load from the db from the given table, column, and rowID + Map* load(std::string table, std::string column, int rowID); + }; + + class DbMapWriter : public MapWriter + { + protected: + sqlite3 *db; + + // checks if input is a safe and valid SQL string + bool isValidSQLInput(std::string input); + + public: + DbMapWriter(Map* map, sqlite3 *db); + + // insert a new map at the given table and column + void write(std::string table, std::string column); + + // update a map at the given table, column, and rowID + void write(std::string table, std::string column, long rowid); + }; }; #endif diff --git a/lib/room.cpp b/lib/room.cpp index 7c04255..3e9abb8 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -32,6 +32,7 @@ Room::Room(RoomType type, int x, int y) void Room::updateTemplate(void) { + // assign room template given the room type if (type == RoomType::Square) { roomTemplate = &_SQUARE_ROOM; @@ -53,6 +54,7 @@ RoomType Room::getType(void) void Room::setType(RoomType type) { + // sets type and updates template this->type = type; updateTemplate(); } @@ -141,6 +143,7 @@ void Room::setDoorOffset(RoomAttribute door, int offset) std::invalid_argument ex("offset is too large."); + // check door type, check for errors, and set offset if (door == RoomAttribute::NorthDoor) { if (abs(offset) > (width-2)/2) @@ -172,10 +175,11 @@ void Room::drawDoors(WINDOW *win) int offset; int width = getWidth(); int length = getHeight(); + // detect doors, find the midpoint + offset and add a space if ((attrs & RoomAttribute::NorthDoor) == RoomAttribute::NorthDoor) { offset = doorOffsets[0]; - mvwaddch(win, y, (x+(width/2)) + offset, ' '); + mvwaddch(win, y, (x+(width/2)) + offset, ' '); } if ((attrs & RoomAttribute::EastDoor) == RoomAttribute::EastDoor) { @@ -196,6 +200,7 @@ void Room::drawDoors(WINDOW *win) void Room::draw(WINDOW *win) { + // iterate through the roomTemplate for (auto it = roomTemplate->begin(); it != roomTemplate->end(); it++) { int ydistance = it - roomTemplate->begin(); diff --git a/lib/room.hpp b/lib/room.hpp index e156259..2a8dc04 100644 --- a/lib/room.hpp +++ b/lib/room.hpp @@ -39,6 +39,7 @@ namespace GrandMaze "#######################" }; + // Bitflags of the room attributes enum RoomAttribute { NorthDoor = 1u, @@ -65,7 +66,9 @@ namespace GrandMaze std::array doorOffsets; const std::vector *roomTemplate; + // Update the internal pointer roomTemplate to the template void updateTemplate(void); + // Draw the doors void drawDoors(WINDOW *win); public: @@ -73,26 +76,41 @@ namespace GrandMaze Room(RoomType type, int x, int y, Colour colour); Room(RoomType type, int x, int y); + // Get room type RoomType getType(void); + // Set room type void setType(RoomType type); + // Get X position int getX(void); + // Get Y position int getY(void); + // Set X position void setX(int x); + // Set Y position void setY(int y); + // Get room width int getWidth(void); + // Get room height int getHeight(void); + // Get room colour Colour getColour(void); + // Set room colour void setColour(Colour colour); + // Get room attributes uint8_t getAttributes(void); + // Set room atrributes void setAttributes(uint8_t attrs); + // Get door offset for a given door int getDoorOffset(RoomAttribute door); + // Set door offset for a given door void setDoorOffset(RoomAttribute door, int offset); + // Draw the room to the window void draw(WINDOW *win); }; };