commit 4aabcfa393f929aeaaa33dd570b43d07fdbf78a0 Author: Leslie Krause Date: Fri Jan 24 11:51:11 2020 -0500 Build 01 - initial commit diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..fed29a3 --- /dev/null +++ b/README.txt @@ -0,0 +1,313 @@ +RocketLib Toolkit v1.1 +By Leslie E. Krause + +RocketLib Toolkit is a purely Lua-driven SQLite3 map reader with an extensive API for +analysis of map databases. The library is intended primarily for use by server operators, +but anybody with Lua programming experience can develop their own command-line tools. + +Just to showcase how easy it is to get started examining your map database, it takes only +15 lines of Lua code to search for all mapblocks that have dropped items: + + require( "maplib" ) + + local map_db = MapDatabase( "~/.minetest/worlds/world/map.sqlite", false ) + + for index, block in map_db.iterate( ) do + local count = 0 + for i, v in ipairs( block.object_list ) do + if v.name == "__builtin:item" then + count = count + 1 + end + end + if count > 0 then + print( string.format( "%d dropped items in mapblock (%s)", + count, index_to_string( index ) + ) ) + end + end + +Important: If your map database exceeds 1GB in size, then a RAM-disk (tmpfs on Linux) is +strongly recommended for optimal performance. Based on personal experience, there can be +upwards of a ten-fold improvement in speed, particularly for intensive queries. + +The map reader library provides a fully object-oriented API, so it is straightfoward to +examine mapblocks and their contents without having to worry about the underlying +database architecture. + +The available class constructors are as follows: + + BlobReader( input ) + Provides an interface to serially parse a BLOB with a variety of known datatypes. + + * input is the BLOB to parse, typically the raw mapblock data obtained from the + "data" field of the "blocks" table + + The BlobReader class defines the following public methods: + + BlobReader::read_u8( ) + Read an 8-bit unsigned integer and advance the pointer by one byte. + + BlobReader::read_u16( ) + Read a 16-bit unsigned integer and advance the pointer by two bytes. + + BlobReader::read_u32( ) + Read a 32-bit unsigned integer and advance the pointer by four bytes. + + BlobReader::read_s16( ) + Read a 16-bit signed integer and advance the pointer by two bytes. + + BlobReader::read_s32( ) + Read a 32-bit signed integer and advance the pointer by four bytes. + + BlobReader::read_f1000( ) + Read a floating point and advance the pointer by four bytes. + + BlobReader::read_v3f1000( ) + Read a 3-dimensional floating point array and advance the pointer by 12 bytes. + + BlobReader::read_zlip( ) + Slurp a zlib compressed data stream and advance the pointer accordingly. + + BlobReader::read_string( len ) + Read a non-terminated text string of len bytes and then advance the pointer + accordingly to len. If len is not provided, slurp a multiline terminated text + string and advance the pointer accordingly. + + MapArea( pos1, pos2 ) + Delineates a mapblock area to be examined, while also providing various area + calculation methods. + + * pos1 is lowest boundary mapblock coordinate to iterate + * pos2 is the highest boundary mapblock coordinate to iterate + + The MapArea class provides the following public methods: + + MapArea::get_min_pos( ) + Return the lowest boundary mapblock position of the area as a table {x,y,z} + + MapArea::get_max_pos( ) + Return the highest boundary mapblock position of the area as a table {x,y,z} + + MapArea::get_volume( ) + Calculate the volume of the area in cubic mapblocks and return the result + + MapArea::has_index( idx ) + Returns true if the specified mapblock hashed position, idx, exists within the area + + MapArea::has_pos( pos ) + Returns true if the specified mapblock position, pos, exists within the area + + MapArea::iterate( ) + Returns an iterator for looping through the area + + MapBlock( blob, is_preview, get_checksum ) + Parses the mapblock data and calculates the associated checksum. For efficiency, the + nodemeta list and the node list are not parsed automatically, but they can be obtained + using the corresponding methods. + + * blob is the raw mapblock data obtained from "data" field of the "blocks" table + * is_preview is a boolean indicating whether to parse the BLOB (optional). + * get_checksum is the checksum function to calculate the checksum and length of the + BLOB (optional). + + The MapBlock class defines the following public methods: + + MapBlock::get_node_list( ) + Parses the raw node list of the mapblock and returns a node_list table. + + The node_list table is an array of exactly 4096 elements, corresponding to the + 16x16x16 matrix of nodes comprising the mapblock. The coordinates of a node can be + obtained using the decode_node_pos( ) helper function. Each entry of the node_list + table contains a subtable with three fields: id, param1, and param2. + + Note that the id refers to the content ID which varies between map blocks. You must + cross-reference the content ID to determine the actual registered node name. + + MapBlock::get_nodemeta_map( ) + Parses the raw nodemeta list and returns a nodemata_list table. + + The nodemeta_map table is an associative array indexed by the position hash of + the corresponding node from the node_list table. Each entry of the nodemeta_list + table is a subtable containing the following fields:e + + * fields is a subtable containing the user-defined metadata for the node, as + ordinary key-value pairs. + * is_private is a boolean specifying whether the metadata of the node is private + * inventory is a subtable containing the inventory of the node as an array of + tables, with two fields for each inventory slot: item_name and item_count + + The MapBlock class defines the following public read-only properties: + + MapBlock::version + The version of the mapblock. + + MapBlock::flags + The flags of the mapblock. + + MapBlock::content_width + The size of the content_ids in bytes. This is either 1 or 2, based on the version. + + MapBlock::params_width + The size of param1 and param2 in bytes. This is always 2. + + MapBlock::object_list + An array of objects stored in the mapblock. Each entry contains a subtable with + seven fields: type, pos, version, name, staticdata, hp, velocity, and yaw. + + MapBlock::nodename_map + An associative array of registered node names indexed by content IDs. + + MapBlock::timestamp + The timetamp when the mapblock was last modified by the engine. Note that this + value is not necessarily a reliable means to determine if a mapblock was changed or + not. For that you should perform a checksum comparison. + + MapDatabase( path, is_preview, summary ) + Opens an existing map.sqlite database from disk and prepares the necessary SQL statements. + + * path is the path to the sqlite3 map database to be opened in read-only mode + * is_preview is a boolean indicating whether mapblocks are to be parsed by default + (optional) + * is_summary is a boolean indicating whether checksums apply to all mapblocks by + default (optional) + + The MapDatabase class defines the following public methods: + + MapDatabase::enable_preview( ) + Enable parsing of mapblocks by default + + MapDatabase::disable_preview( ) + Disable parsing of mapblocks by default, only calculate checksum and length + + MapDatabase::enable_summary( ) + Enable cumulative checksum calculations by default + + MapDatabase::disable_summary( ) + Disable cumulative checksum calculations by default + + MapDatabase::change_algorithm( algorithm ) + Switches to a different checksum algorithm, either 'adler32' or 'crc32'. + + MapDatabase::create_cache( use_memory, on_step ) + Create a cache database storing cross-references of mapblock position hashes, + thereby speeding up successive queries. If use_memory is true, the cache database + will be memory resident. Otherwise a file named "map.sqlite-cache" will be created + in the same directory as the map database. The optional on_step hook can be used to + update a progress bar for lengthy operations. + + MapDatabase::get_length( ) + Returns the total number of mapblocks. If the cache is available, then it will be + used. + + MapDatabase::get_area_length( area ) + Returns the total number of mapblocks inside the given area. The cache is required + for this operation. + + MapDatabase::iterate( on_step ) + Returns an iterator function, for looping over all existing mapblocks. The optional + on_step hook can be used to update a progress bar for length operations + + MapDatabase::iterate_area( area, on_step ) + Returns an iterator function, for looping over all existing mapblocks inside the + given area. The optional on_step hook can be used to update a progress bar for + lengthy operations. The cache is required for this operation. + + MapDatabase::select( on_step ) + Returns an array of all hashed positions for all mapblocks. The optional on_step + hook can be used to update a progress bar for lengthy operations. The cache is not + used for this operation (but I will consider making it optional) + + MapDatabase::select_area( area, on_step ) + Returns an array of hashed positions for all mapblocks inside the given area. The + optional on_step hook can be used to update a progress bar for lengthy operations. + The cache is required for this operation. + + MapDatabase::has_index( index ) + Returns a boolean indicating whether a mapblock exists at the given hashed + position. + + MapDatabase::get_mapblock( index, get_checksum ) + Returns the mapblock at the given hashed position as a MapBlock object. The + optional get_checksum function will be used to calculate the checksum and length + of the BLOB. + + MapDatabase::get_mapblock_raw( index, get_checksum ) + Returns the mapblock as a BLOB, without calculating the checksum. + + MapDatabase::close( index, get_checksum ) + Closes the map database (but it doesn't close the cache database, which is a known + unresolved bug). + +Several helper functions are also available for debugging and conversion purposes. + + decode_pos( index ) + Converts the given mapblock hashed position to a vector position. + + encode_pos( pos ) + Converts the given mapblock vector position to a hashed position. + + decode_node_pos( node_index, index ) + Converts the given node index within the given mapblock to a vector position in world + coordinates. If the index parameter is not provided, then the result will be relative + to a mapblock at {0,0,0}. Note: For consistency with Lua conventions, node indexes + are always 1-based. + + encode_node_pos( node_pos ) + Converts the given node vector position into a both a node index and mapblock hashed + position. + + encode_pos( pos ) + Converts the given mapblock vector position to a hashed position. + + pos_to_string( pos ) + Returns a string representing the given vector position as "x,y,z". + + dump( buffer ) + Returns a string representing a memory dump of the given buffer + + +Repository +---------------------- + +Browse source code... + https://bitbucket.org/sorcerykid/rocketlib + +Download archive... + https://bitbucket.org/sorcerykid/rocketlib/get/master.zip + https://bitbucket.org/sorcerykid/rocketlib/get/master.tar.gz + +Installation +---------------------- + +RocketLib Toolkit depends on the Lua modules lsqlite3 and zblip, which can be installed +using Luarocks. + +Luarocks itself can be obtained from +https://github.com/luarocks/luarocks/wiki/Download + + +License of source code +---------------------------------------------------------- + +The MIT License (MIT) + +Copyright (c) 2020, Leslie Krause (leslie@searstower.org) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this +software and associated documentation files (the "Software"), to deal in the Software +without restriction, including without limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of the Software, and to permit +persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or +substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + +For more details: +https://opensource.org/licenses/MIT diff --git a/rocketlib.lua b/rocketlib.lua new file mode 100644 index 0000000..75d0820 --- /dev/null +++ b/rocketlib.lua @@ -0,0 +1,619 @@ +----------------------------------------------------- +-- Minetest :: RocketLib Toolkit (rocketlib) +-- +-- See README.txt for licensing and release notes. +-- Copyright (c) 2018-2020, Leslie E. Krause +----------------------------------------------------- + +package.cpath = package.cpath .. ";/usr/local/lib/lua/5.1/?.so" + +local zlib = require( "zlib" ) -- https://luarocks.org/modules/brimworks/lua-zlib +local sqlite3 = require( "lsqlite3complete" ) -- https://luarocks.org/modules/dougcurrie/lsqlite3 + +----------------------------- +-- Conversion Routines +----------------------------- + +local floor = math.floor +local ceil = math.ceil +local max = math.max +local min = math.min +local byte = string.byte +local match = string.match +local find = string.find +local sub = string.sub + +local function to_signed( val ) + return val < 2048 and val or val - 2 * 2048 +end + +function decode_pos( idx ) + local x = to_signed( idx % 4096 ) + idx = floor( ( idx - x ) / 4096 ) + local y = to_signed( idx % 4096 ) + idx = floor( ( idx - y ) / 4096 ) + local z = to_signed( idx % 4096 ) + return { x = x, y = y, z = z } +end + +function encode_pos( pos ) + return pos.x + pos.y * 4096 + pos.z * 16777216 +end + +function decode_node_pos( node_idx, idx ) + local pos = idx and decode_pos( idx ) or { x = 0, y = 0, z = 0 } + local node_pos = { } + + node_idx = node_idx - 1 -- correct for one-based indexing used in node_list + + node_pos.x = ( node_idx % 16 ) + pos.x * 16 + node_idx = floor( node_idx / 16 ) + node_pos.y = ( node_idx % 16 ) + pos.y * 16 + node_idx = floor( node_idx / 16 ) + node_pos.z = ( node_idx % 16 ) + pos.z * 16 + + return node_pos +end + +function encode_node_pos( node_pos ) + local pos = { + x = floor( node_pos.x / 16 ), + y = floor( node_pos.y / 16 ), + z = floor( node_pos.z / 16 ) + } + local x = node_pos.x % 16 + local y = node_pos.x % 16 + local z = node_pos.x % 16 + return x + y * 16 + z * 256, encode_pos( pos ) +end + +local function is_match( text, glob ) + -- use array for captures + _ = { match( text, glob ) } + return #_ > 0 and _ or nil +end + +----------------------------- +-- Debugging Routines +----------------------------- + +function pos_to_string( pos ) + return string.format( "(%d,%d,%d)", pos.x, pos.y, pos.z ) +end + +function dump( buffer ) + for i = 1, ceil( #buffer / 16 ) * 16 do + if ( i - 1 ) % 16 == 0 then io.write( string.format( '%08X ', i - 1 ) ) end + io.write( i > #buffer and ' ' or string.format( '%02x ', buffer:byte( i ) ) ) + if i % 8 == 0 then io.write( ' ' ) end + if i % 16 == 0 then io.write( buffer:sub( i - 16 + 1, i ):gsub( '%c', '.' ), '\n' ) end + end +end + +----------------------------- +-- BlobReader Class +----------------------------- + +function BlobReader( input ) + local idx = 1 + local self = { } + + -- private methods + + local function u16_to_signed( val ) + return val < 32767 and val or val - 2 * 32767 + end + local function u32_to_signed( val ) + return val < 2147483647 and val or val - 2 * 2147483647 + end + + -- public methods + + self.read_u8 = function ( ) + local output = byte( input, idx ) + idx = idx + 1 + return output + end + self.read_u16 = function ( ) + -- 16-bit unsigned integer + local output = byte( input, idx ) * 256 + byte( input, idx + 1 ) + idx = idx + 2 + return output + end + self.read_u32 = function ( ) + -- 32-bit unsigned integer + local output = byte( input, idx ) * 16777216 + byte( input, idx + 1 ) * 65536 + byte( input, idx + 2 ) * 256 + byte( input, idx + 3 ) + idx = idx + 4 + return output + end + self.read_s16 = function ( ) + -- 16-bit signed integer + local output = u16_to_signed( byte( input, idx ) * 256 + byte( input, idx + 1 ) ) + idx = idx + 2 + return output + end + self.read_s32 = function ( ) + -- 32-bit signed integer + local output = u32_to_signed( byte( input, idx ) * 16777216 + byte( input, idx + 1 ) * 65536 + byte( input, idx + 2 ) * 256 + byte( input, idx + 3 ) ) + idx = idx + 4 + return output + end + self.read_f1000 = function ( ) + local output = self.read_s32( ) / 1000 + return output + end + self.read_v3f10000 = function ( ) + local output = { x = self.read_s32( ) / 10000, y = self.read_s32( ) / 10000, z = self.read_s32( ) / 10000 } + return output + end + self.read_zlib = function ( ) + output, is_eof, bytes_in, bytes_out = zlib.inflate( )( sub( input, idx ) ) + idx = idx + bytes_in + return output + end + self.read_string = function ( len ) + if not len then + -- multiline string + local len = find( input, "\n", idx ) - idx + local output = sub( input, idx, idx + len - 1 ) + idx = idx + len + 1 + return output + else + -- non-terminated string + local output = sub( input, idx, idx + len - 1 ) + idx = idx + len + return output + end + end + return self +end + +----------------------------- +-- Deserializer Routines +----------------------------- + +local function parse_node_list( blob ) + local p = BlobReader( blob ) + local this = { } + + for idx = 1, 4096 do + this[ idx ] = { id = p.read_s16( ) } + end + for idx = 1, 4096 do + this[ idx ].param1 = p.read_u8( ) + end + for idx = 1, 4096 do + this[ idx ].param2 = p.read_u8( ) + end + + return this +end + +local function parse_nodemeta_map( blob ) + local p = BlobReader( blob ) + local this = { } + + local version = p.read_u8( ) + if version == 0 then + return this + elseif version > 3 then + error( "Unsupported node_metadata version, aborting!" ) + end + + local node_total = p.read_u16( ) + for node_count = 1, node_total do + local pos = p.read_u16( ) + 1 -- use one-based indexing to correspond with node_list array + local var_total = p.read_u32( ) + + this[ pos ] = { fields = { }, inventory = { }, is_private = false } + + for var_count = 1, var_total do + local key = p.read_string( p.read_u16( ) ) -- 16-bit length string + local value = p.read_string( p.read_u32( ) ) -- 32-bit length string + this[ pos ].fields[ key ] = value + end + if version >= 2 then + this[ pos ].is_private = ( p.read_u8( ) == 1 ) + end + for inv_count = 1, 127 do + local text = p.read_string( ) + + if text == "EndInventory" then + break + end + + if is_match( text, "^Item ([a-zA-Z0-9_]+:[a-zA-Z_]+)$" ) or is_match( text, "^Item ([a-zA-Z0-9_]+:[a-zA-Z0-9_]+) (%d+)" ) then + table.insert( this[ pos ].inventory, { item_name = _[ 1 ], item_count = tonumber( _[ 2 ] ) or 1 } ) + elseif text == "Empty" then + table.insert( this[ pos ].inventory, { } ) -- empty item stack + end + end + end + return this +end + +local function parse_object( blob ) + local p = BlobReader( blob ) + local this = { } + + this.version = p.read_u8( ) + this.name = p.read_string( p.read_u16( ) ) + this.staticdata = p.read_string( p.read_u32( ) ) + + if this.version == 1 then + this.hp = p.read_s16( ) + this.velocity = p.read_f1000( ) / 10 + this.yaw = p.read_f1000( ) / 10 + end + return this +end + +------------------------ +-- MapBlock Class +------------------------ + +function MapBlock( blob, is_preview, get_checksum ) + local self = { } + + self.checksum, self.length = get_checksum( blob ) + + if is_preview then return self end + + ---------- + + local p = BlobReader( blob ) + + self.version = p.read_u8( ) + self.flags = p.read_u8( ) + if self.version >= 27 then + self.lighting_complete = p.read_u16( ) + end + self.content_width = p.read_u8( ) + self.params_width = p.read_u8( ) + if self.params_width ~= 2 then + error( "Invalid params_width, aborting!" ) + end + if self.content_width ~= 1 and self.version < 24 or self.content_width ~= 2 and self.version >= 24 then + error( "Invalid params_width, aborting!" ) + end + + ---------- + + local node_list_raw = p.read_zlib( ) + if #node_list_raw ~= 4096 * self.content_width * self.params_width then + error( "Invalid node_list, aborting!" ) + end + + ---------- + + local nodemeta_list_raw = p.read_zlib( ) + + ---------- + + if self.version == 23 then + p.read_u8( ) -- unused + end + + ---------- + + self.object_list = { } + + local obj_version = p.read_u8( ) + local obj_total = p.read_u16( ) + for obj_count = 1, obj_total do + local type = p.read_u8( ) + local pos = p.read_v3f10000( ) + local blob = p.read_string( p.read_u16( ) ) + + local object = parse_object( blob ) + object.type = type + object.pos = pos + + table.insert( self.object_list, object ) + end + + self.timestamp = p:read_u32( ) + + ---------- + + self.nodename_map = { } + + local map_version = p.read_u8( ) + local map_total = p.read_u16( ) + for map_total = 1, map_total do + local id = p.read_s16( ) + local name = p.read_string( p.read_u16( ) ) + self.nodename_map[ id ] = name + end + + ---------- + + -- TODO: parse timers + + ---------- + + self.get_node_list = function ( ) + return parse_node_list( node_list_raw ) + end + self.get_nodemeta_map = function ( ) + return parse_nodemeta_map( nodemeta_list_raw ) + end + + return self +end + +----------------------------- +-- MapArea Class +----------------------------- + +function MapArea( pos1, pos2 ) + local self = { } + + -- presort positions and clamp to designated boundaries + local x1 = max( min( pos1.x, pos2.x ), -2048 ) + local y1 = max( min( pos1.y, pos2.y ), -2048 ) + local z1 = max( min( pos1.z, pos2.z ), -2048 ) + local x2 = min( max( pos1.x, pos2.x ), 2048 ) + local y2 = min( max( pos1.y, pos2.y ), 2048 ) + local z2 = min( max( pos1.z, pos2.z ), 2048 ) + + self.get_min_pos = function ( ) + return { x = x1, y = y1, z = z1 } + end + + self.get_max_pos = function ( ) + return { x = x2, y = y2, z = z2 } + end + + self.get_volume = function ( ) + return ( z2 - z1 + 1 ) * ( y2 - y1 + 1 ) * ( x2 - x1 + 1 ) + end + + self.has_index = function ( idx ) + local x = to_signed( idx % 4096 ) + if x < x1 or x > x2 then return false end + + idx = floor( ( idx - x ) / 4096 ) + local y = to_signed( idx % 4096 ) + if y < y1 or y > y2 then return false end + + idx = floor( ( idx - y ) / 4096 ) + local z = to_signed( idx % 4096 ) + if z < z1 or z > z2 then return false end + + return true + end + + self.has_pos = function ( pos ) + if pos.x < x1 or pos.x > x2 or pos.y < y1 or pos.y > y2 or pos.z < z1 or pos.z > z2 then + return false + end + return true + end + + self.iterate = function ( ) + local x + local y = y1 + local z = z1 + return function ( ) + if not x then + x = x1 + elseif x < x2 then + x = x + 1 + elseif y < y2 then + x = x1 + y = y + 1 + elseif z < z2 then + x = x1 + y = y1 + z = z + 1 + else + return nil + end + return x + y * 4096 + z * 16777216 + end + end + + return self +end + +----------------------------- +-- MapDatabase Class +----------------------------- + +function MapDatabase( path, is_preview, is_summary ) + local self = { } + local map_db = sqlite3.open( path, sqlite3.OPEN_READONLY ) + local cache_db + local init_checksum = zlib.crc32 + + if not map_db then + error( "Cannot open map database, aborting!" ) + end + + local map_select_pos = map_db:prepare( "SELECT data FROM blocks WHERE pos = ?" ) + local map_select = map_db:prepare( "SELECT pos, data FROM blocks" ) + + self.enable_preview = function ( ) + is_preview = true + end + + self.disable_preview = function ( ) + is_preview = false + end + + self.enable_summary = function ( ) + is_summary = true + end + + self.disable_summary = function ( ) + is_summary = false + end + + self.change_algorithm = function ( algorithm ) + init_checksum = ( { ["crc32"] = zlib.crc32, ["adler32"] = zlib.alder32 } )[ algorithm ] + end + + self.create_cache = function ( use_memory, on_step ) + cache_db = use_memory and sqlite3.open_memory( ) or sqlite3.open( path .. "-cache" ) + + if not cache_db then + error( "Cannot open cache database, aborting!" ) + end + for _ in cache_db:rows( "SELECT * FROM sqlite_master WHERE name = 'catalog' and type = 'table'" ) do return end + + if cache_db:exec( "CREATE TABLE catalog (pos INTEGER PRIMARY KEY, x INTEGER, y INTEGER, z INTEGER)" ) ~= sqlite3.OK then + error( "Cannot update cache database, aborting!" ) + end + + local stmt = cache_db:prepare( "INSERT INTO catalog VALUES (?, ?, ?, ?)" ) + + cache_db:exec( "BEGIN" ) -- combine into single transaction + for index in map_db:urows( "SELECT pos FROM blocks" ) do + if on_step then on_step( ) end + + local pos = decode_pos( index ) + + stmt:reset( ) + stmt:bind_values( index, pos.x, pos.y, pos.z ) + if stmt:step( ) ~= sqlite3.DONE then + error( "Cannot update cache database, aborting!" ) + end + end + cache_db:exec( "END" ) + end + + self.get_length = function ( ) + if not cache_db then + for total in map_db:urows( "SELECT count(*) FROM blocks" ) do + return total + end + else + for total in cache_db:urows( "SELECT count(*) FROM catalog" ) do + return total + end + end + end + + self.get_area_length = function ( area ) + if not cache_db then return end + + local stmt = cache_db:prepare( "SELECT count(*) FROM catalog WHERE x >= ? AND x <= ? AND y >= ? AND y <= ? AND z >= ? AND z <= ?" ) + local min_pos = area.get_min_pos( ) + local max_pos = area.get_max_pos( ) + + stmt:bind_values( min_pos.x, max_pos.x, min_pos.y, max_pos.y, min_pos.z, max_pos.z ) + for total in stmt:urows( ) do + return total + end + end + + self.iterate = function ( on_step ) + local get_checksum + local stmt = map_select + stmt:reset( ) + + if is_summary then + get_checksum = init_checksum( ) + end + + return function ( ) + if stmt:step( ) ~= sqlite3.ROW then return end + + if on_step then on_step( ) end + + local index = stmt:get_value( 0 ) + local block = MapBlock( stmt:get_value( 1 ), is_preview, get_checksum or init_checksum( ) ) + return index, block + end + end + + self.iterate_area = function ( area, on_step ) + if not cache_db then return end + + -- provided cache database, query indices of all mapblocks within + -- area, then return each index and parsed block + local stmt = cache_db:prepare( "SELECT pos FROM catalog WHERE x >= ? AND x <= ? AND y >= ? AND y <= ? AND z >= ? AND z <= ?" ) + + local min_pos = area.get_min_pos( ) + local max_pos = area.get_max_pos( ) + + stmt:bind_values( min_pos.x, max_pos.x, min_pos.y, max_pos.y, min_pos.z, max_pos.z ) + + if is_summary then + get_checksum = init_checksum( ) + end + + return function ( ) + if stmt:step( ) ~= sqlite3.ROW then + stmt:finalize( ) + return + end + if on_step then on_step( ) end + + local index = stmt:get_value( 0 ) + local block = self.get_mapblock( index, get_checksum or init_checksum( ) ) + return index, block + end + end + + self.select = function ( on_step ) + for index in map_db:urows( "SELECT pos FROM blocks" ) do + if on_step then on_step( ) end + + table.insert( index_list, index ) + end + return index_list + end + + self.select_area = function ( area, on_step ) + local index_list = { } + + local stmt = cache_db:prepare( "SELECT pos FROM catalog WHERE x >= ? AND x <= ? AND y >= ? AND y <= ? AND z >= ? AND z <= ?" ) + + local min_pos = area.get_min_pos( ) + local max_pos = area.get_max_pos( ) + + stmt:bind_values( min_pos.x, max_pos.x, min_pos.y, max_pos.y, min_pos.z, max_pos.z ) + for index in stmt:urows( ) do + if on_step then on_step( ) end + table.insert( index_list, index ) + end + + return index_list + end + + self.has_index = function ( index ) + local stmt = map_select_pos + + stmt:reset( ) + stmt:bind_values( index ) + return stmt:step( ) == sqlite3.ROW + end + + self.get_mapblock = function ( index ) + local stmt = map_select_pos + + stmt:reset( ) + stmt:bind_values( index ) + if stmt:step( ) == sqlite3.ROW then + return MapBlock( stmt:get_value( 0 ), is_preview, init_checksum( ) ) + end + end + + self.get_mapblock_raw = function ( index ) + local stmt = map_select_pos + + stmt:reset( ) + stmt:bind_values( index ) + if stmt:step( ) == sqlite3.ROW then + return stmt:get_value( 0 ) + end + end + + self.close = function ( ) + map_db:close( ) + end + + return self +end