commit
4aabcfa393
|
@ -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
|
|
@ -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
|
Loading…
Reference in New Issue