444 lines
11 KiB
Lua
444 lines
11 KiB
Lua
--[[
|
|
|
|
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
|