/*:
@plugindesc [v1.0] Load your books from dedicated book files instead of the Plugin Manager or JSON.
@author Solar Flare Games
@param directory
@text Books Directory
@desc The directory under data/ to load books from. All .txt files in this directory will be loaded as books.
@type text
@default books/
@param catDirs
@text Categories as Subdirectories
@desc Whether or not to use subdirectories as categories. If false, subdirectories are ignored.
@type boolean
@default true
@param catOrder
@text Category Order
@desc Specifies the sort order for the categories.
@type text[]
@default []
@param bkgnd
@text Backgrounds
@desc Specifies custom backgrounds for each of your books.
@type struct<BookBkgnd>[]
@param inline
@text Inline Images
@desc List images included with %img so that Exclude Unused Files knows about them.
@type file[]
@require 1
@dir img/pictures/
@default []
@help
This is an addon to TAA_BookMenu which allows you to load your books from
dedicated book files rather than a set of JSON files or directly from the
Plugin Manager.
For each book, create a simple .txt file with the book's contents. The filename
will be used as the book's key for ReadBook and other commands, with any spaces
stripped out. By default, it will also become the book's title.
Be warned: if you plan to deploy to the web or mobile, this plugin is not
guaranteed to work.
Also note: Even if you use subdirectories as categories, you need to make sure
that books in different directories do not have the same name!
A book file consists of two sections. The first section contains configuration
details about the book, while the second section consists of the book's
actual content. The two sections are separated by a line containing
three or more hyphens, like this:
---------
The configuration section consists of a series of "key: value" pairs, one
per line. The following keys can be used:
• title: Sets the book's title
• titleColor: Corresponds to Title Window Text Color
• category: Specifies the book's category
• id: Specifies the book's sorting key; if left out, books will be sorted
alphabetically by their key (filename).
You cannot set custom backgrounds in the file. This is done to suppoort the
Exclude Unused Files option in RMMV deployment. Instead, you set custom
backgrounds here in the Plugin Manager. The Backgrounds parameter is a list
of possible backgrounds, each of which also has a list of books which it should
be used for - use the book's filename without the .txt extension.
If you want the same background on two books but with a different mode, you
can add that background to the list multiple times without issue. However,
do note that if you specify the same book on more than one background entry,
only the first will take effect.
*//*~struct~BookBkgnd:
@param books
@text Books
@desc The books to apply this background to.
@type text[]
@default []
@param bkgnd
@text Custom Background
@desc Specifies the custom background to use for these books.
@type file
@require 1
@dir img/pictures/
@param mode
@text Custom Background Mode
@desc Define how the custom image should be used for these books.
@type select
@option Detached Text Window Only
@value 5
@option Detached Title + Text Window
@value 9
@option Menu Text Window Only
@value 6
@option Menu Title + Text Window
@value 10
@option All Text Window Only
@value 7
@option All Title + Text Window
@value 11
@default 11
*/
(function() {
var params = PluginManager.parameters('SFG_BookFiles');
var backgrounds = Utils.parseRecursive(params.bkgnd);
const readBooksFromDirectory = function(dir, mode) {
let fs_base = require('fs'), util = require('util');
// This works around Node being old and lacking the 'fs/promises' module.
let fs = {
exists: util.promisify(fs_base.exists),
stat: util.promisify(fs_base.stat),
readdir: util.promisify(fs_base.readdir),
readFile: util.promisify(fs_base.readFile),
};
fs.exists(dir).then(exists => {
if(!exists) return; // No books to load...
return fs.stat(dir);
}).then(stat => {
if(!stat.isDirectory()) return; // No books to load...
return fs.readdir(dir);
}).then(dirListing => {
if(dirListing.length == 0) return; // No books to load...
let stats = [];
for(let i = 0; i < dirListing.length; i++) {
let path = dir + dirListing[i];
stats.push(fs.stat(path).then(stat => {
return {
file: dirListing[i],
path: path,
isDir: stat.isDirectory(),
};
}));
}
return Promise.all(stats);
}).then(files => {
if(mode == '?categories') {
files = files.filter(f => f.isDir);
for(let i = 0; i < files.length; i++) {
let path = files[i].path;
if(!path.endsWith('/')) path += '/';
readBooksFromDirectory.call(this, path, files[i].file);
}
return [];
} else {
files = files.filter(f => !f.isDir).map(f => f.path);
let data = [];
for(let i = 0; i < files.length; i++) {
data.push(fs.readFile(files[i], 'utf8').then(data => {
return {
file: files[i].replace(/.*\//g, ''),
path: files[i],
data: data,
};
}))
}
return Promise.all(data);
}
}).then(files => {
if(files.length == 0) return; // No books to load...
for(let i = 0; i < files.length; i++) {
let file = files[i].file;
let data = files[i].data.split(/^-{3,}\r?\n/m);
if(data.length > 2) {
console.warn('More than one section separator in book file %1'.format(file))
}
let book = {
id: 0, customBg: '', titleColor: 0,
title: file.replace('.txt', ''),
category: mode === '?all' ? params.unknownCategory : mode,
};
let header = data[0], bookKey = file.replace(/\s+/g, '').replace('.txt', '');
// Backgrounds...
for(let j = 0; j < backgrounds.length; j++) {
if(backgrounds[j].includes(bookKey)) {
let bg = backgrounds[j];
book.customBg = bg.bkgnd;
book.customBgMode = bg.mode;
break;
}
}
book.text = data.slice(1).join('------').replace(/\r\n/g, '\n');
for(let line of header.split('\n')) {
let m = line.match(/^([a-z]+):(.*)$/im);
if(m) {
let key = m[1].trim().toLowerCase(), value = m[2].trim();
if(key === 'category' && mode !== '?all') continue;
book[key] = value;
}
}
this._books[bookKey] = book;
// Create category subobject
if(!this._bookKeyByCategory[book.category]) {
this._bookKeyByCategory[book.category] = [];
}
if(!this._booksRead[book.category]) {
this._booksRead[book.category] = [];
}
// Fill in books under categories ordered by IDs
if(this._bookKeyByCategory[book.category][book.id] === undefined) {
if(!this._bookKeyByCategory[book.category].contains(bookKey))
this._bookKeyByCategory[book.category][book.id] = bookKey;
} else {
if(!this._bookKeyByCategory[book.category].contans(bookKey))
this._bookKeyByCategory[book.category].push(bookKey);
}
if(!this._categoryByBookKey[bookKey])
this._categoryByBookKey[bookKey] = book.category;
}
this._doneLoading = true;
});
};
const old_loadBooks = LibraryData.prototype.loadBookData;
LibraryData.prototype.loadBookData = function() {
if(this._source === 'Book Files') {
if(!Utils.isNwjs()) {
throw Error("Node not available, can't load books!");
}
let dir = 'data/%1'.format(params.directory);
if(!dir.endsWith('/')) dir += '/';
this._bookKeyByCategory = {}
this._booksRead = {};
this._books = {};
this._categoryByBookKey = {};
this._doneLoading = false;
readBooksFromDirectory.call(this, dir, params.catDirs ? '?categories' : '?all');
this._categoryList = JSON.parse(params.catOrder);
this._bookTitleObject = "title";
this._bookMenuTitleColorObject = "titleColor";
this._bookTextObject = "text";
this._bookCategoryObject = "category";
this._bookIdObject = "id";
// TODO: Set custom backgrounds from params.bkgnd...
} else old_loadBooks.call(this);
};
})();