--[[ sqlite backend for insidethebox This backend stores various persistent world data bits that are not properly stored by the minetest engine. This mod provides both persistent and performant data storage for the itb game until a better solution exists. Major data storage parts in this db: - players, player scores No player metadata storage exists currently. Only HP and air are stored in player data. We lack storage for things like skin choice, stamina, RPG points and skills, etc. - box data --]] -- gratuitous banner minetest.log("action", " _|_|_| _| _|") minetest.log("action", " _| _|_|_| _|_|_| _|_|_| _|_|") minetest.log("action", " _| _| _| _|_| _| _| _| _|_|_|_|") minetest.log("action", " _| _| _| _|_| _| _| _| _|") minetest.log("action", " _|_|_| _| _| _|_|_| _| _|_|_| _|_|_|") minetest.log("action", "") minetest.log("action", " _| _| _|") minetest.log("action", " _|_|_|_| _|_|_| _|_| _|_|_| _|_| _| _|") minetest.log("action", " _| _| _| _|_|_|_| _| _| _| _| _|_|") minetest.log("action", " _| _| _| _| _| _| _| _| _| _|") minetest.log("action", " _|_| _| _| _|_|_| _|_|_| _|_| _| _|") local world_path = minetest.get_worldpath() -- local sqlite3 = require("lsqlite3") local insecure_env = minetest.request_insecure_environment() local sqlite3 = insecure_env.require("lsqlite3") local itb_db = sqlite3.open(world_path .. "/itb.sqlite") if not itb_db then assert("lsqlite3 not available or non-functional. Please use e.g. luarocks to install lsqlite3") end -- we require these tables to exist: assert(itb_db:exec[[ CREATE TABLE box (id INTEGER PRIMARY KEY, data BLOB); CREATE TABLE box_meta (id INTEGER PRIMARY KEY, type INTEGER, meta BLOB); CREATE TABLE series (id INTEGER PRIMARY KEY, name TEXT, meta BLOB); CREATE TABLE series_box (series_id INTEGER, box_id INTEGER, box_order INTEGER); CREATE TABLE player (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE, meta BLOB); CREATE TABLE points (player_id INTEGER, score REAL, type TEXT, box_id INTEGER); ]]) -- table structure and content --[[ box table: Contain the storage of the box content (not metadata) so all blocks. (one-to-one) - id: box identifier - data: box contents (nodes) series: Contains existing series - id: series ID - name: name of the series - meta: misc data series_box: maps boxes into series (many-to-many) - series_id: series ID - box_id: box ID - box_order: series are sorted by increasing order player: player info - id: player number - name: player name - meta: random player data bits score: player and box score data - player_id: player id - score: score value - type: score type (e.g. reviewed a box) - box: associated box ID --]] db = {} function db.player_get_meta(name) local stmt = itb_db:prepare[[ SELECT meta FROM player WHERE name = :name ]] stmt:bind_names{name = name} local it, state = stmt:nrows() local row = it(state) stmt:finalize() if row then local meta = minetest.parse_json(row.meta) if not meta.series_progress then meta.series_progress = {} end return meta end minetest.log("error", "db.player_get_meta(" .. name .. "): no such player in db") return {series_progress = {}} end function db.player_set_meta(name, meta) assert(name ~= "") local stmt = itb_db:prepare[[ REPLACE INTO player (name, meta) VALUES (:name, :meta) ]] stmt:bind_names{name = name, meta = minetest.write_json(meta)} local r = stmt:step() stmt:finalize() if not r then minetest.log("error", "db.player_set_meta(" .. name .. "): error writing") return false else return true end end function db.series_create(name) assert(name ~= "") local stmt = itb_db:prepare[[ INSERT INTO series (name, meta) VALUES(:name, :meta) ]] stmt:bind_names{ name = name, meta = minetest.write_json({ meta = {} }) } local r = stmt:step() stmt:finalize() if not r then minetest.log("error", "db.series_create(" .. name .. "): error writing") return nil end local lastrow = itb_db:last_insert_rowid() if lastrow ~= 0 then local stmt2 = itb_db:prepare[[ SELECT id FROM series WHERE rowid = :lastrow ]] stmt2:bind_names{lastrow = lastrow} local it, state = stmt2:nrows() local row = it(state) stmt2:finalize() if row then return row.id end end minetest.log("error", "db.series_create(" .. name .. "): failed") return nil end function db.series_destroy(series_id) assert(series_id) local stmt = itb_db:prepare[[ DELETE FROM series WHERE id = :series_id ]] stmt:bind_names{series_id = series_id} local r = stmt:step() stmt:finalize() if not r then minetest.log("error", "db.series_remove(" .. series_id .. "): error") return nil end return true end function db.series_get_series() local stmt = itb_db:prepare[[ SELECT id, name FROM series ORDER BY id ]] local series = {} local series_count = 1 for row in stmt:nrows() do series[series_count] = {id = row.id, name = row.name} series_count = series_count + 1 end stmt:finalize() return series end function db.series_get_meta(series_id) local stmt = itb_db:prepare[[ SELECT name, meta FROM series WHERE id = :series_id ]] stmt:bind_names{series_id = series_id} local it, state = stmt:nrows() local row = it(state) stmt:finalize() if row then local result = { name = row.name, meta = minetest.parse_json(row.meta), } return result end minetest.log("error", "db.series_get_meta(" .. series_id .. "): no such series meta for that id") return nil end function db.series_set_meta(series_id, meta) local stmt = itb_db:prepare[[ REPLACE INTO series (id, name, meta) VALUES(:series_id, :name, :meta) ]] stmt:bind_names{ series_id = series_id, name = meta.name, meta = minetest.write_json(meta.meta), } local r = stmt:step() stmt:finalize() if not r then minetest.log("error", "db.series_set_meta(" .. series_id .. "): error writing") return false else return true end end function db.series_get_boxes(series_id) local stmt = itb_db:prepare[[ SELECT box_id FROM series_box WHERE series_id = :series_id ORDER BY box_order ]] stmt:bind_names{series_id = series_id} local boxes = {} local boxes_index = 1 for row in stmt:nrows() do boxes[boxes_index] = row.box_id boxes_index = boxes_index + 1 end stmt:finalize() return boxes end function db.series_insert_box(series_id, box_id, box_order) local stmt = itb_db:prepare[[ INSERT INTO series_box (series_id, box_id, box_order) VALUES(:series_id, :box_id, :box_order) ]] stmt:bind_names{ series_id = series_id, box_id = box_id, box_order = box_order, } local r = stmt:step() stmt:finalize() if not r then minetest.log("error", "db.series_insert_box(" .. series_id .. ", " .. box_id .."): error writing") return false else return true end end function db.series_add_at_end(series_id, box_id) local stmt = itb_db:prepare[[ SELECT MAX(box_order) FROM series_box WHERE series_id = :series_id ]] stmt:bind_names{series_id = series_id} local mx = 0 for row in stmt:nrows() do mx = row["MAX(box_order)"] or 0 end stmt:finalize() return db.series_insert_box(series_id, box_id, mx + 1) end function db.series_delete_box(series_id, box_id) local stmt = itb_db:prepare[[ DELETE FROM series_box WHERE series_id = :series_id AND box_id = :box_id ]] stmt:bind_names{ series_id = series_id, box_id = box_id, } local r = stmt:step() stmt:finalize() if not r then minetest.log("error", "db.series_delete_box(" .. series_id .. ", " .. box_id .. "): error writing") return false else return true end end function db.box_get_data(box_id) local stmt = itb_db:prepare[[ SELECT data FROM box WHERE id = :box_id ]] stmt:bind_names{box_id = box_id} local it, state = stmt:nrows() local row = it(state) stmt:finalize() if row then return row.data end minetest.log("error", "db.box_get_data(" .. box_id .."): no such box") return nil end function db.box_set_data(box_id, data) local stmt = itb_db:prepare[[ REPLACE INTO box (id, data) VALUES(:box_id, :box_data) ]] stmt:bind_names{box_id = box_id, box_data = data} local r = stmt:step() stmt:finalize() if not r then minetest.log("error", "db.box_set_data(" .. box_id .."): error writing") return false else return true end end db.BOX_TYPE = 0 db.ENTRY_TYPE = 1 db.EXIT_TYPE = 2 local last_box_id = -1 for row in itb_db:nrows[[ SELECT MAX(id) FROM box_meta ]] do last_box_id = row["MAX(id)"] or 0 --otherwise crashes on first startup? end function db.get_last_box_id() return last_box_id end function db.box_exists(box_id) local stmt = itb_db:prepare[[ SELECT id FROM box_meta WHERE id = :box_id ]] stmt:bind_names{box_id = box_id} local it, state = stmt:nrows() local row = it(state) stmt:finalize() if row then return true else return false end end function db.box_get_meta(box_id) local stmt = itb_db:prepare[[ SELECT type, meta FROM box_meta WHERE id = :box_id ]] stmt:bind_names{box_id = box_id} local it, state = stmt:nrows() local row = it(state) stmt:finalize() if row then local result = { type = row.type, meta = minetest.parse_json(row.meta), } if result.type == db.BOX_TYPE and not result.meta.num_items then result.meta.num_items = 1 end if result.type == db.BOX_TYPE and (not result.meta.time or result.meta.num_completed_players == 0) then result.meta.num_completed_players = 0 result.meta.completed_players = {} result.meta.time = { player_best = {}, overall_best = 1e100, average_first = 0, average_best = 0, } result.meta.deaths = { player_best = {}, overall_best = 1e100, average_first = 0, average_best = 0, } result.meta.damage = { player_best = {}, overall_best = 1e100, average_first = 0, average_best = 0, } end if result.type == db.BOX_TYPE and not result.meta.box_name then result.meta.box_name = "(No name)" end if result.type == db.BOX_TYPE and not result.meta.skybox then result.meta.skybox = 0 end if result.type == db.BOX_TYPE and not result.meta.build_time then result.meta.builder = "" result.meta.build_time = 0 end if result.type == db.EXIT_TYPE and not result.meta.signs_to_update then result.meta.signs_to_update = {} end if result.type == db.EXIT_TYPE and not result.meta.star_positions then result.meta.star_positions = {} result.meta.category_positions = {} end return result end minetest.log("error", "db.box_get_data(" .. box_id .. "): no such box meta") return nil end function db.box_set_meta(box_id, meta) local stmt = itb_db:prepare[[ REPLACE INTO box_meta (id, type, meta) VALUES(:box_id, :box_type, :box_meta) ]] stmt:bind_names{ box_id = box_id, box_type = meta.type, box_meta = minetest.write_json(meta.meta), } local r = stmt:step() stmt:finalize() last_box_id = math.max(last_box_id, box_id) if not r then minetest.log("error", "db.box_set_data(" .. box_id .. "): error writing") return false else return true end end function db.shutdown() itb_db:close() end