From 27710ca81edcdb75b6e37fc940861840e11304ed Mon Sep 17 00:00:00 2001 From: JSKitty Date: Sat, 14 Mar 2026 19:00:30 +0000 Subject: [PATCH] Lazy-compile books on open instead of batch-compiling at boot Previously, all 34 books were compiled (text reflow + pagination) at startup. This involved reading every .txt file, parsing a 192KB JSON cache for byte-for-byte comparison, and either loading or recompiling all books before the main menu appeared. The entire process took roughly 3 seconds on every boot, wasting CPU cycles on content the player may never read. This refactor defers compilation to the moment a book is actually opened. At startup, createBooks() now only discovers filenames via PhysFS without reading any file contents. When a book is opened for the first time, compileBook() loads that single .txt file, normalizes whitespace, reflows text to fit page dimensions, and paginates. The compiled pages are cached in memory, so subsequent opens of the same book are instant. Performance results (MacBook Pro M4 Max, 128GB): - Boot time reduced by ~3 seconds (book init now runs in under 1ms) - Individual book compilation is imperceptible (microseconds per book) - No observable hitch when opening any book, including multi-page ones Code reduction: - Removed ~620 lines of cache management (readCompiledBooks, writeCompiledBooks, booksRequireCompiling, readBooksIntoTemp) - Removed compiled_books.json read/write/validation infrastructure - Removed rapidjson/filereadstream.h dependency from book system - Added ~45 lines for the lazy compilation path The change is platform-agnostic with no preprocessor conditionals. Book indices, mod reload (physfsReloadBooks), banned book filtering, and multiplayer sync are all unaffected since allBooks is populated in the same sorted order and numbooks is set at discovery time. --- src/book.cpp | 695 ++------------------------------------ src/book.hpp | 8 +- src/interface/bookgui.cpp | 7 + 3 files changed, 44 insertions(+), 666 deletions(-) diff --git a/src/book.cpp b/src/book.cpp index 506a38df60..c94b463daa 100644 --- a/src/book.cpp +++ b/src/book.cpp @@ -15,25 +15,11 @@ #include "interface/interface.hpp" #include "book.hpp" #include "rapidjson/document.h" -#include "rapidjson/filereadstream.h" #include "player.hpp" #include "ui/Text.hpp" #include "ui/Field.hpp" #include "mod_tools.hpp" -namespace -{ - -constexpr int32_t TAB_WIDTH = 3; - -} - -//#define SPACE_NINJA_NAME "Order of the Space Ninjas" -//Any word less than this will just get bumped onto the next line. -#define MIN_LENGTH_TO_SPLIT_WORD (book_characterspace_x / 2) -//#define MIN_LENGTH_TO_SPLIT_WORD_TITLE (characterspace_x / 2) //This only works in the formatTitle() function, since it uses a local variable characterspace_x in there. -//#define SPLIT_WORD_IN_TITLE false //Whether or not to split a word in the book's title. If set to false, will only split words if they have to be split. If set to true, will split words if they're a minimum length of MIN_LENGTH_TO_SPLIT_WORD_TITLE.; - std::vector allBooks; BookParser_t bookParser_t; int numbooks = 0; @@ -111,109 +97,6 @@ void BookParser_t::deleteBooks() numbooks = 0; } -bool BookParser_t::readCompiledBooks() -{ - std::string compiledBooksPath = "books/compiled_books.json"; - if ( PHYSFS_getRealDir(compiledBooksPath.c_str()) != NULL ) - { - std::string path = PHYSFS_getRealDir(compiledBooksPath.c_str()); - path.append(PHYSFS_getDirSeparator()); - compiledBooksPath = path + compiledBooksPath; - File* fp = FileIO::open(compiledBooksPath.c_str(), "rb"); - if ( fp ) - { - static char buf[MAX_FILE_LENGTH]; - int count = fp->read(buf, sizeof(buf[0]), sizeof(buf)); - buf[count] = '\0'; - rapidjson::StringStream is(buf); - FileIO::close(fp); - - rapidjson::Document d; - d.ParseStream(is); - if ( !d.HasMember("version") || !d.HasMember("books") ) - { - printlog("[JSON]: Could not read member 'version' or 'books', possible invalid syntax."); - return false; - } - - int numBooksRead = 0; - for ( rapidjson::Value::ConstMemberIterator book_itr = d["books"].MemberBegin(); - book_itr != d["books"].MemberEnd(); ++book_itr ) - { - allBooks.push_back(Book_t()); - auto& newBook = allBooks[allBooks.size() - 1]; - newBook.default_name = book_itr->name.GetString(); - for ( rapidjson::Value::ConstValueIterator page_itr = book_itr->value["pages"].Begin(); - page_itr != book_itr->value["pages"].End(); ++page_itr ) - { - newBook.formattedPages.push_back(page_itr->GetString()); - } - ++numBooksRead; - } - printlog("[Books]: Read %d precompiled books successfully.", numBooksRead); - return true; - } - } - return false; -} - -bool BookParser_t::booksRequireCompiling() -{ - readBooksIntoTemp(); - - std::string compiledBooksPath = "books/compiled_books.json"; - if ( PHYSFS_getRealDir(compiledBooksPath.c_str()) != NULL ) - { - std::string path = PHYSFS_getRealDir(compiledBooksPath.c_str()); - path.append(PHYSFS_getDirSeparator()); - compiledBooksPath = path + compiledBooksPath; - File* fp = FileIO::open(compiledBooksPath.c_str(), "rb"); - if ( fp ) - { - static char buf[MAX_FILE_LENGTH]; - int count = fp->read(buf, sizeof(buf[0]), sizeof(buf)); - buf[count] = '\0'; - rapidjson::StringStream is(buf); - FileIO::close(fp); - - rapidjson::Document d; - d.ParseStream(is); - if ( !d.HasMember("version") || !d.HasMember("books") ) - { - printlog("[JSON]: Could not read member 'version' or 'books', possible invalid syntax."); - return false; - } - - if ( d["books"].MemberCount() != tempBookData.size() ) - { - printlog("[Books]: Compiled Books Check - Found %d compiled books, but %d in directory, recompiling...", d["books"].MemberCount(), tempBookData.size()); - return true; - } - - for ( rapidjson::Value::ConstMemberIterator book_itr = d["books"].MemberBegin(); - book_itr != d["books"].MemberEnd(); ++book_itr ) - { - std::string bookName = book_itr->name.GetString(); - std::string rawText = book_itr->value["raw_text"].GetString(); - - if ( tempBookData.find(bookName) == tempBookData.end() ) - { - printlog("[Books]: Compiled Books Check - Book title: \"%s\" not found in compiled books, recompiling...", bookName.c_str()); - return true; - } - else - { - if ( tempBookData[bookName] != rawText ) - { - printlog("[Books]: Compiled Books Check - Book text: \"%s\" does not match in compiled books, recompiling...", bookName.c_str()); - return true; - } - } - } - } - } - return false; -} std::list BookParser_t::getListOfBooksAfterFiltering() { @@ -301,316 +184,75 @@ std::list BookParser_t::getListOfBooksAfterFiltering() return discoveredbooks; } -void BookParser_t::readBooksIntoTemp() -{ - tempBookData.clear(); - - std::list discoveredbooks = getListOfBooksAfterFiltering(); - - if ( !discoveredbooks.empty() ) - { - printlog("[Books]: Read %d books into temporary storage...", discoveredbooks.size()); - // read books - for ( const auto& filename : discoveredbooks ) - { - //printlog("reading book: \"%s\"\n", filename.c_str()); - std::string filenameNoExtension = filename; - auto findExtension = filename.find(".txt"); - if ( findExtension != std::string::npos ) - { - filenameNoExtension = filename.substr(0, findExtension); - } - tempBookData.insert(std::make_pair(filenameNoExtension, "")); - auto& entry = tempBookData[filenameNoExtension]; - - //Load in the text from a file. - std::string bookPath = "books/"; - bookPath.append(filename); - if ( PHYSFS_getRealDir(bookPath.c_str()) != nullptr ) - { - std::string path = PHYSFS_getRealDir(bookPath.c_str()); - path.append(PHYSFS_getDirSeparator()); - bookPath = path + bookPath; - } - - char bookChar[PATH_MAX]; - strncpy(bookChar, bookPath.c_str(), PATH_MAX - 1); - char* tmp = readFile(bookChar); - if ( tmp ) - { - entry = tmp; - } - free(tmp); - } - } - else - { - printlog("[Books]: Warning - no books were discovered."); - } -} - void BookParser_t::createBooks(bool forceCacheRebuild) { deleteBooks(); // empty the old books array - if ( !forceCacheRebuild && !booksRequireCompiling() ) - { - if ( readCompiledBooks() ) - { - numbooks = allBooks.size(); - return; - } - printlog("[Books]: Error - Failed to read pre-compiled books... recompiling."); - } - std::list discoveredbooks = getListOfBooksAfterFiltering(); - // create books for ( const auto& filename : discoveredbooks ) { - printlog("[Books]: Compiling book: \"%s\"\n", filename.c_str()); - createBook(filename); - } - - if ( discoveredbooks.empty() ) - { - printlog( "[Books]: Warning: discoveredbooks is empty. No books in /books/ directory?\n"); - } - - numbooks = allBooks.size(); - - writeCompiledBooks(); -} - -void BookParser_t::writeCompiledBooks() -{ - std::string inputPath = outputdir; - inputPath.append(PHYSFS_getDirSeparator()); - std::string fileName = "books/compiled_books.json"; - inputPath.append(fileName); - - File* fp = FileIO::open(inputPath.c_str(), "rb"); - rapidjson::Document d; - if ( !fp ) - { - printlog("[JSON]: Could not locate json file %s, creating new file.", inputPath.c_str()); - d.SetObject(); - CustomHelpers::addMemberToRoot(d, "version", rapidjson::Value(versionJSON)); - } - else - { - char buf[MAX_FILE_LENGTH]; - int count = fp->read(buf, sizeof(buf[0]), sizeof(buf)); - buf[count] = '\0'; - rapidjson::StringStream is(buf); - FileIO::close(fp); - d.ParseStream(is); - - if ( !d.HasMember("version") ) + allBooks.push_back(Book_t()); + auto& book = allBooks.back(); + book.default_name = filename; + auto ext = book.default_name.find(".txt"); + if ( ext != std::string::npos ) { - printlog("[JSON]: Could not read member 'version', possible invalid syntax."); - printlog("[Books]: Error: Failed to compile books into file: '%s'", inputPath.c_str()); - return; + book.default_name = book.default_name.substr(0, ext); } } - if ( d.HasMember("books") ) - { - d.EraseMember("books"); - } - rapidjson::Value booksObj(rapidjson::kObjectType); - CustomHelpers::addMemberToRoot(d, "books", booksObj); - - for ( auto& book : allBooks ) - { - if ( !d["books"].HasMember(book.default_name.c_str()) ) - { - rapidjson::Value bookObj(rapidjson::kObjectType); - CustomHelpers::addMemberToSubkey(d, "books", book.default_name.c_str(), bookObj); - - rapidjson::Value rawTextKey("raw_text", d.GetAllocator()); - rapidjson::Value rawTextVal(book.text.c_str(), d.GetAllocator()); - d["books"][book.default_name.c_str()].AddMember(rawTextKey, rawTextVal, d.GetAllocator()); - - rapidjson::Value pagesKey("pages", d.GetAllocator()); - rapidjson::Value pagesArrayVal(rapidjson::kArrayType); - d["books"][book.default_name.c_str()].AddMember(pagesKey, pagesArrayVal, d.GetAllocator()); - for ( auto& page : book.formattedPages ) - { - rapidjson::Value pageVal; - pageVal.SetString(page.c_str(), d.GetAllocator()); - d["books"][book.default_name.c_str()]["pages"].PushBack(pageVal, d.GetAllocator()); - } - } - else - { - d["books"][book.default_name.c_str()]["raw_text"].SetString(book.text.c_str(), d.GetAllocator()); - d["books"][book.default_name.c_str()]["pages"].Clear(); - for ( auto& page : book.formattedPages ) - { - rapidjson::Value pageVal; - pageVal.SetString(page.c_str(), d.GetAllocator()); - d["books"][book.default_name.c_str()]["pages"].PushBack(pageVal, d.GetAllocator()); - } - } - } - - fp = FileIO::open(inputPath.c_str(), "wb"); - if ( !fp ) - { - printlog("[Books]: Error: Failed to compile books into file: '%s'", inputPath.c_str()); - return; - } - rapidjson::StringBuffer os; - rapidjson::PrettyWriter writer(os); - d.Accept(writer); - fp->write(os.GetString(), sizeof(char), os.GetSize()); - FileIO::close(fp); - - printlog("[Books]: Successfully compiled books into file: '%s'", inputPath.c_str()); -} - -/*****createBook() helper functions******/ - -bool isLetter(const char character) -{ - return (character >= 'A' && character <= 'Z') || (character >= 'a' && character <= 'z'); -} - -//This is a more powerful version of isLetter that checks if a specified character is part of a word. However, it requires contextual information -- what are the next and previous characters? So pass the entire string to this function and the index in the string of the character being looked up. -bool isCharacterPartOfWord(char* const text, const int index) -{ - if (!text) - { - return false; - } - if (index < 0 || index > strlen(text)) - { - return false; - } - - if (isLetter(text[index])) - { - return true; //It's a character. Return true because no use continuing this function. - } - - switch (text[index]) - { - case '\'': - if (isLetter(text[index - 1]) && isLetter(text[index + 1])) //An apostrophe needs to be surrounded by letters to qualify as part of a word. - { - return true; - } - return false; - default: - return false; - } -} + numbooks = allBooks.size(); -int moveToStartOfWord(char* const text, const int index) -{ - if (!text) - { - return index; - } - if (index < 0 || index > strlen(text)) + if ( numbooks == 0 ) { - return index; + printlog("[Books]: Warning: no books discovered in /books/ directory.\n"); } - - int i = index; - - //Keep running backwards until it finds a non-letter character (which would be the character before the start of the word). - for (; i - 1 > 0; --i) + else { - if (!isCharacterPartOfWord(text, i - 1)) - { - return i; - } + printlog("[Books]: Discovered %d books.\n", numbooks); } - - return i; } -//Returns 0 on error. Returns 0 if index not on a word. Returns length of word otherwise. If nonletter character at the current index, it keeps looking until it finds the start of the next word. -int lengthOfCurrentWord(char* const text, const int index) +void BookParser_t::compileBook(int index) { - if (!text) //Can't do this without text. + if ( index < 0 || index >= (int)allBooks.size() ) { - return 0; - } - if (index < 0 || index > strlen(text)) //Index has to be in bounds. - { - return 0; - } - - int length = 0; - int i = index; - - if (!isCharacterPartOfWord(text, i)) //The current character is not part of a word. - //i = moveToNextWord(text, index); //Find the start of the next word. - { - return 0; //Not our problem. Tell it the current word is length 0. - } - - if (i > 0 && isCharacterPartOfWord(text, i)) //Not at the start of the text array and the previous character is a letter...yikes, not at the start of the word. - { - i = moveToStartOfWord(text, i); //Move to the start of the current word. + return; } - - for (; i < strlen(text); ++i) + auto& book = allBooks[index]; + if ( !book.formattedPages.empty() ) { - if (isCharacterPartOfWord(text, i)) - { - length++; //The current character is part of the word. - } - else - { - return length; //Reached the end of the word. Return the length. - } + return; // already compiled } - return length; -} - -void BookParser_t::createBook(std::string filename) -{ - //Load in the text from a file. - std::string tempstr = "books/"; - tempstr.append(filename); - if ( PHYSFS_getRealDir(tempstr.c_str()) != nullptr ) + // Reconstruct the .txt filename from the stored default_name + std::string filename = book.default_name + ".txt"; + std::string bookPath = "books/"; + bookPath.append(filename); + if ( PHYSFS_getRealDir(bookPath.c_str()) != nullptr ) { - std::string path = PHYSFS_getRealDir(tempstr.c_str()); + std::string path = PHYSFS_getRealDir(bookPath.c_str()); path.append(PHYSFS_getDirSeparator()); - tempstr = path + tempstr; + bookPath = path + bookPath; } - allBooks.push_back(Book_t()); - auto& newBook = allBooks[allBooks.size() - 1]; - char bookChar[PATH_MAX]; - strncpy(bookChar, tempstr.c_str(), PATH_MAX - 1); + strncpy(bookChar, bookPath.c_str(), PATH_MAX - 1); char* tmp = readFile(bookChar); if ( tmp ) { - newBook.text = tmp; + book.text = tmp; } free(tmp); - if ( newBook.text == "" ) + if ( book.text.empty() ) { - printlog( "error opening book \"%s\".\n", tempstr.c_str()); - return; //Failed to open the file. + printlog("error opening book \"%s\".\n", bookPath.c_str()); + return; } - newBook.default_name = filename; - auto findTxt = newBook.default_name.find(".txt"); - if ( findTxt != std::string::npos ) - { - newBook.default_name = newBook.default_name.substr(0, findTxt); - } - //newBook.rawBookText = book->text; - - std::string pageText = ""; - for ( auto& character : newBook.text ) + // Normalize: strip \r, expand \t to 3 spaces + std::string pageText; + for ( auto& character : book.text ) { if ( character == '\r' ) { @@ -658,279 +300,14 @@ void BookParser_t::createBook(std::string filename) if ( textHeight > tmpField->getSize().h ) { // exceeds size, move to next page. - newBook.formattedPages.push_back(pageText); + book.formattedPages.push_back(pageText); pageText = ""; } } while ( (token = nexttoken) != NULL ); - newBook.formattedPages.push_back(pageText); + book.formattedPages.push_back(pageText); - if ( tmpField ) - { - delete tmpField; - tmpField = nullptr; - } - if ( reflowedText ) - { - free(reflowedText); - reflowedText = nullptr; - } - return; - - //const Uint32 color = makeColor( 0, 0, 0, 255); - //strncpy(string->data, pageText.c_str(), 120); - //int len = strlen(pageText.c_str()); - //string->data[120] = '\0'; - //printlog(string->data); - //return; - ////for ( int c = 0; reflowedText[c] != '\0'; ++c ) - ////{ - //// string->data[c] = reflowedText[c]; - ////} - //char* nexttoken = nullptr; - //char* token = reflowedText; - //do { - // nexttoken = Field::tokenize(token, "\n"); - // if ( !pageText.empty() ) - // { - // pageText.push_back('\n'); - // } - // if ( token ) - // { - // for ( int c = 0; token[c] != '\0'; ++c ) - // { - // if ( token[c] == '\r' ) - // { - // continue; - // } - // if ( token[c] == '\t' ) - // { - // for ( int insertTabs = TAB_WIDTH; insertTabs > 0; --insertTabs ) - // { - // pageText += ' '; - // } - // } - // else - // { - // pageText += token[c]; - // } - // } - // } - // tmpField.setText(pageText.c_str()); - // if ( auto getText = Text::get(tmpField.getText(), tmpField.getFont(), - // makeColor(255, 255, 255, 255), makeColor(0, 0, 0, 255)) ) - // { - // if ( getText->getHeight() > tmpField.getSize().h - tmpField.getSize().y ) - // { - // // exceeds size, move to next page. - // for ( int p = 0; p < pageText.size(); ++p ) - // { - // string->data[p] = pageText[p]; - // } - // //strcpy(string->data, pageText.c_str()); - // int len = strlen(pageText.c_str()); - // string->data[len] = '\0'; - - // string = newString(&book->pages, color, nullptr); - // string->data = static_cast(malloc(sizeof(char) * (max_characters + 1))); - // memset(string->data, 0, sizeof(char) * (max_characters + 1)); - - // pageText = ""; - // } - // } - - //} while ( (token = nexttoken) != NULL ); - //len = strlen(pageText.c_str()); - //for ( int p = 0; p < pageText.size(); ++p ) - //{ - // string->data[p] = pageText[p]; - //} - ////strcpy(string->data, pageText.c_str()); - //string->data[len] = '\0'; - //free(reflowedText); - //return; - - //string_t* string = newString(&book->pages, color, nullptr); - //string->data = static_cast(malloc(sizeof(char) * (max_characters + 1))); - //memset(string->data, 0, sizeof(char) * (max_characters + 1)); - - //int p = 0; // current character in the page's text - //int x = 0; // number of characters written on the current line - //int y = 0; // number of lines on the page - //bool newline = false; - //bool can_write = true; //If false, it means that the character write to page was interecepted by a - to properly break up a word. - //int tab = 0; //Inserting tab characters. - //char character_to_record = ' '; - //bool write_out = true; - ////found_word and word_length are used to prevent smaller words from being broken up. When the for loop detects that it has hit the start of a word, it queries for the word's length. If the word's length < MIN_LENGTH_TO_SPLIT_WORD, then it pumps out a newline and then starts the word. word_length_left is there so that it knows how many more characters it has to go through to reach the end of the word. - //bool found_word = false; - //int word_length = 0; - //for (int i = 0; book->text[i] != 0; ++i) - //{ - // //So first iterate through and count every line. - // //Line end conditions: - // //1. Encountered a newline character. - // //2. Hit the max amount of characters that can fit horizontally in the book GUI. - // write_out = true; - // newline = false; - // can_write = true; - - // //When the max amount of lines that can fit on one page have been hit, create a new page. - // if (tab <= 0 && book->text[i] == '\t') //Assuming it is a null terminated string. - // { - // //Found a newline. Create a new line. - // tab = TAB_WIDTH; - // } - // if (x + 1 >= book_characterspace_x) - // { - // //Overflowed the line. Go onto the next line. - // newline = true; - // } - // else if (tab <= 0 && book->text[i] == '\n') - // { - // newline = true; - // write_out = false; //Do not want to write out this /n. - // } - - // character_to_record = book->text[i]; - - // if (newline) - // { - // x = 0; //Reset x since it's back to the beginning of the line. - // if (y + 1 >= book_characterspace_y) - // { - // //Create the next page. Do not record the character if it's a newline. - // string = newString(&book->pages, color, nullptr); - // string->data = static_cast(malloc(sizeof(char) * (max_characters + 1))); - // memset(string->data, 0, sizeof(char) * (max_characters + 1)); - // p = 0; - // y = 0; - // } - // else - // { - // //Record the new line. - // string->data[p] = '\n'; - // p++; - // y++; - // //No need to record the item in book->text[i] because it was a newline. - // } - // //Bugger the tab away if it hit the end of the line. - // if (tab > 0) - // { - // if (tab < TAB_WIDTH) - // { - // i--; //Do this too? Or i++? I don't know. - // } - // tab = 0; - // } - // // no spaces at the start of a line - // if ( character_to_record == ' ' ) - // { - // continue; - // } - // } - // else if (tab > 0) - // { - // character_to_record = ' '; - // } - - // if (write_out) - // { - // //Ok, record the character. - - // //First, check if it's found a word. - // if (!found_word && tab <= 0) //Don't check for a word if already found one (to save processing) and don't bother checking if tab mode is active. - // { - // if (isCharacterPartOfWord(book->text, i)) - // { - // //The character is part of a word. Record that. - // found_word = true; - // word_length = lengthOfCurrentWord(book->text, i); - - // //Now check if the word will fit on the line and if it's okay to split it. If not okay to split it, make a new line. - // if (word_length <= MIN_LENGTH_TO_SPLIT_WORD) - // { - // //The word doesn't like being split. - // if ((x + 1) + word_length >= book_characterspace_x && word_length < book_characterspace_x) - // { - // //So make a new line. - // x = 0; //Reset x, since it's writing out a new line and x gets reset when we do that. - // if (y + 1 >= book_characterspace_y) //Check if it hit the end of the page. - // { - // //It does indeed go off the page. Start a new page. - // string = newString(&book->pages, color, nullptr); - // string->data = static_cast(malloc(sizeof(char) * (max_characters + 1))); - // memset(string->data, 0, sizeof(char) * (max_characters + 1)); - // p = 0; - // y = 0; - // } - // else - // { - // string->data[p] = '\n'; - // p++; - // y++; - // } - // } - // } - // } - // } - // else if (found_word) - // { - // if (!isCharacterPartOfWord(book->text, i)) - // { - // //Reached the end of the word. - // found_word = false; - // word_length = 0; - // } - // } - - // if (x + 2 >= book_characterspace_x) - // { - // if (isCharacterPartOfWord(book->text, i)) - // { - // if (book->text[i - 1] != ' ') //First check if the previous character is not a space. - // { - // //If it is not a space (and the like), write out a space since this one is character, the previous one isn't, and the newline is interrupting a word. - // string->data[p] = '-'; - // p++; - // x++; - // can_write = false; - // i--; - // } - // else if (isCharacterPartOfWord(book->text, i + 1)) //Okay, so the previous character is a space (or otherwise not constituting a word. Check if the next character is not a space (or the like)). - // { - // //Next character is not a space (and the like), write out a space so that the word's start is delayed until a safer point in time. - // string->data[p] = ' '; - // p++; - // x++; - // can_write = false; - // i--; - // } - // } - // } - - // if (can_write) - // { - // (string->data)[p] = character_to_record; - // p++; - // x++; - // } - // else - // { - // can_write = true; - // } - - // if (tab > 0) - // { - // if (tab < TAB_WIDTH) - // { - // i--; //To make sure it doesn't bugger the character. - // } - // tab--; - // } - // } - - // newline = false; - //} + delete tmpField; + free(reflowedText); } diff --git a/src/book.hpp b/src/book.hpp index c3a5241121..ef1f42f795 100644 --- a/src/book.hpp +++ b/src/book.hpp @@ -26,16 +26,10 @@ extern std::vector allBooks; class BookParser_t { static const int MAX_FILE_LENGTH = 192000; - const int versionJSON = 1; public: - bool booksRequireCompiling(); - bool readCompiledBooks(); - void writeCompiledBooks(); void createBooks(bool forceCacheRebuild); - void createBook(std::string filename); //Take a book filename and generate all of its pages. + void compileBook(int index); // Lazy-compile a single book's pages on demand. void deleteBooks(); - std::unordered_map tempBookData; - void readBooksIntoTemp(); std::list getListOfBooksAfterFiltering(); }; extern BookParser_t bookParser_t; diff --git a/src/interface/bookgui.cpp b/src/interface/bookgui.cpp index 14204435e6..e8b8862a1b 100644 --- a/src/interface/bookgui.cpp +++ b/src/interface/bookgui.cpp @@ -536,6 +536,13 @@ void Player::BookGUI_t::openBook(int index, Item* item) openBookItem = item; currentBookPage = 0; + // Lazy-compile: paginate this book on first open + int bookIdx = getBook(openBookName); + if ( bookIdx >= 0 && (size_t)bookIdx < allBooks.size() ) + { + bookParser_t.compileBook(bookIdx); + } + playSound(83 + local_rng.rand() % 6, 156); // add the book to the list of read books