Overhaul everything
This commit is contained in:
parent
3796be4ecb
commit
2e1c7455e5
187
README.md
187
README.md
@ -6,87 +6,194 @@ Map database editor for Minetest
|
|||||||
|
|
||||||
MapEdit is a command-line tool written in Python for relatively fast manipulation of Minetest map database files. Functionally, it is similar to WorldEdit, but it is designed for handling very large tasks which would be unfeasible for doing with WorldEdit.
|
MapEdit is a command-line tool written in Python for relatively fast manipulation of Minetest map database files. Functionally, it is similar to WorldEdit, but it is designed for handling very large tasks which would be unfeasible for doing with WorldEdit.
|
||||||
|
|
||||||
The tool is currently in the beta stage; it is not complete and likely contains bugs. Use it at your own risk.
|
MapEdit is currently in the beta stage, and like any code, it may have bugs. Use it at your own risk.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
MapEdit requires Python 3. All other required packages should already be bundled with Python. Only sqlite database files are supported at the moment, but support for more formats may be added in the future.
|
- Python 3 (If you don't already have it, download it from [python.org](https://www.python.org).)
|
||||||
|
- NumPy, which can be installed with `pip install numpy`.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
**A note about mapblocks**
|
#### About mapblocks
|
||||||
|
|
||||||
MapEdit's area selection only operates on whole mapblocks. A single mapblock is a 16x16x16 node area of the map, similar to Minecraft's chunks. The lower southwestern corner of a mapblock is always at coordinates which are evenly divisible by 16, e.g. (32, 64, -48) or the like.
|
Minetest stores and transfers map data in *mapblocks*, which are similar to Minecraft's *chunks*. A single mapblock is a cubical, 16x16x16 node area of the map. The lower southwestern corner (-X, -Y, -Z) of a mapblock is always at coordinates divisible by 16, e.g. (0, 16, -48) or the like.
|
||||||
|
|
||||||
**A note about parameters**
|
Mapblocks are stored in a *map database*, usually `map.sqlite`.
|
||||||
|
Most commands require mapblocks to be already generated to work. This can be acheived by either exploring the area in-game, or by using Minetest's built-in `/emergeblocks` command.
|
||||||
|
|
||||||
All string-like parameters can safely be surrounded with quotes if they happen to contain spaces.
|
#### General usage
|
||||||
|
|
||||||
**General usage**
|
`python mapedit.py [-h] -f <file> [--no-warnings] <command>`
|
||||||
|
|
||||||
`python mapedit.py [-h] -f <file> [-s <file>] [--p1 x y z] [--p2 x y z] [--inverse] [--silencewarnings] <command>`
|
#### Arguments
|
||||||
|
|
||||||
**Parameters**
|
- **`-h`**: Show a help message and exit.
|
||||||
|
- **`-f <file>`**: Path to primary map file. This should be the `map.sqlite` file in the world directory. Note that only SQLite databases are currently supported. This file will be modified, so *always* shut down the game/server before executing the command.
|
||||||
|
- **`--no-warnings`**: Don't show safety warnings or confirmation prompts. For those who feel brave.
|
||||||
|
- **`<command>`**: Command to execute. See "Commands" section below.
|
||||||
|
|
||||||
**`-h`**: Show a help message and exit.
|
#### Common command arguments
|
||||||
|
|
||||||
**`-f <file>`**: Path to primary map file. This should be the `map.sqlite` file in the world dircetory. This file will be modified, so *always* shut down the game/server before executing the command.
|
- **`--p1, --p2`**: Used to select an area with corners at `p1` and `p2`, similar to how WorldEdit's area selection works. It doesn't matter what what sides of the area p1 and p2 are on, as long as they are opposite each other.
|
||||||
|
- **Node/item names**: includes `searchnode`, `replacenode`, etc. Must be the full name, e.g. "default:stone", not just "stone".
|
||||||
|
|
||||||
**`-s <file>`**: Path to secondary map file. This is used by the `overlayblocks` command.
|
#### Other tips
|
||||||
|
|
||||||
**`--p1 x y z --p2 x y z`**: This selects an area with corners at `p1` and `p2`, similar to how WorldEdit's area selection works. Only mapblocks which are fully contained within the area will be selected. Currently, this only applies to the cloneblocks, deleteblocks, fillblocks, and overlayblocks commands.
|
String-like arguments can be surrounded with quotes if they contain spaces.
|
||||||
|
|
||||||
**`--inverse`**: Invert the selection. All mapblocks will be selected except those *fully* within the selected area.
|
MapEdit will often leave lighting glitches. To fix these, use Minetest's built-in `/fixlight` command, or the equivalent WorldEdit `//fixlight` command.
|
||||||
|
|
||||||
**`--silencewarnings`**: Silence all safety warnings.
|
|
||||||
|
|
||||||
**`<command>`**: Command to execute.
|
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
**`cloneblocks --offset x y z`**
|
### `clone`
|
||||||
|
|
||||||
Clones (copies) the given area and moves it by `offset`. The new cloned mapblocks will replace any mapblocks which already existed in that area. Note: the value of `offset` is *rounded down* to the nearest whole number of mapblocks.
|
**Usage:** `clone --p1 x y z --p2 x y z --offset x y z [--blockmode]`
|
||||||
|
|
||||||
**`deleteblocks`**
|
Clone (copy) the given area to a new location. By default, nothing will be copied into mapblocks that are not yet generated.
|
||||||
|
|
||||||
Deletes all mapblocks within the given area. Note: Deleting mapblocks is *not* the same as replacing them with air. Mapgen will be invoked where the blocks were deleted, and this sometimes causes terrain glitches.
|
Arguments:
|
||||||
|
|
||||||
**`fillblocks <name>`**
|
- **`--p1, --p2`**: Area to copy from.
|
||||||
|
- **`--offset`**: Offset to shift the area by. For example, to copy an area 50 nodes upward (positive Y direction), use `--offset 0 50 0`.
|
||||||
|
- **`--blockmode`**: If present, only blocks *fully* inside the area will be cloned, and `offset` will be rounded to the nearest multiple of 16. In this mode, mapblocks may also be copied into non-generated areas. May be signifigantly faster for large areas.
|
||||||
|
|
||||||
Fills all mapblocks within the given area with node `name`, similar to WorldEdit's `set` command. Currently, fillblocks only operates on existing mapblocks and does not actually generate new ones. It also usually causes lighting glitches.
|
### `overlay`
|
||||||
|
|
||||||
**`overlayblocks`**
|
**Usage:** `overlay [--p1 x y z] [--p2 x y z] [--invert] [--offset x y z] [--blockmode] <input_file>`
|
||||||
|
|
||||||
Selects all mapblocks within the given area in the secondary map file, and copies them to the same location in the primary map file. The cloned mapblocks will replace existing ones.
|
Copy part or all of an input map file into the primary file. By default, nothing will be copied into mapblocks that are not yet generated.
|
||||||
|
|
||||||
**`replacenodes <searchname> <replacename>`**
|
Arguments:
|
||||||
|
|
||||||
Replaces all nodes of name `searchname` with node `replacename`, without affecting lighting, param2, metadata, or node timers. To delete the node entirely, use `air` as the replace name. This can take a long time for large map files or very common nodes, e.g. dirt.
|
- **`input_file`**: Path to input map file.
|
||||||
|
- **`--p1, --p2`**: Area to copy from. If not specified, MapEdit will try to copy everything from the input map file.
|
||||||
|
- **`--invert`**: If present, copy everything *outside* the given area.
|
||||||
|
- **`--offset`**: Offset to move nodes by when copying; default is no offset. This currently cannot be used with an inverted selection.
|
||||||
|
- **`--blockmode`**: If present, copy whole mapblocks instead of node regions. Only blocks fully inside or fully outside the given area will be copied, depending on whether `--invert` is used. In addition, `offset` will be rounded to the nearest multiple of 16. May be signifigantly faster for large areas.
|
||||||
|
|
||||||
**`setparam2 <searchname> <value>`**
|
### `deleteblocks`
|
||||||
|
|
||||||
Set the param2 value of all nodes with name `searchname` to `value`.
|
**Usage:** `deleteblocks --p1 x y z --p2 x y z [--invert]`
|
||||||
|
|
||||||
**`deletemeta <searchname>`**
|
Deletes all mapblocks in the given area.
|
||||||
|
|
||||||
Delete all metadata of nodes with name `searchname`. This includes node inventories as well.
|
**Note:** Deleting mapblocks is *not* the same as filling them with air! Mapgen will be invoked where the blocks were deleted, and this sometimes causes terrain glitches.
|
||||||
|
|
||||||
**`setmetavar <searchname> <key> <value>`**
|
Arguments:
|
||||||
|
|
||||||
Set the metadata variable `key` to `value` of all nodes with name `searchname`. This only affects nodes which already have the given variable in their metadata.
|
- **`--p1, --p2`**: Area to delete from. Only mapblocks fully inside this area will be deleted.
|
||||||
|
- **`--invert`**: Delete only mapblocks that are fully *outside* the given area.
|
||||||
|
|
||||||
**`replaceininv <searchname> <searchitem> <replaceitem> [--deletemeta]`**
|
### `fill`
|
||||||
|
|
||||||
Replaces all items with name `searchitem` with item `replaceitem` in the inventories of nodes with name `searchname`. To delete an item entirely, *do not* replace it with air—instead, use the keyword `Empty` (capitalized). Include the `--deletemeta` flag to delete the item's metadata when replacing it.
|
**Usage:** `fill --p1 x y z --p2 x y z [--invert] [--blockmode] <replacenode>`
|
||||||
|
|
||||||
**`deletetimers <searchname>`**
|
Fills the given area with one node. The affected mapblocks must be already generated for fill to work.
|
||||||
|
This command does not currently affect param2, node metadata, etc.
|
||||||
|
|
||||||
Delete all node timers of nodes with name `searchname`.
|
Arguments:
|
||||||
|
|
||||||
**`deleteobjects [--item] <searchname>`**
|
- **`replacenode`**: Name of node to fill the area with.
|
||||||
|
- **`--p1, --p2`**: Area to fill.
|
||||||
|
- **`--invert`**: Fill everything *outside* the given area.
|
||||||
|
- **`--blockmode`**: Fill whole mapblocks instead of node regions. Only mapblocks fully inside the region (or fully outside, if `--invert` is used) will be filled. This option currenly has little effect.
|
||||||
|
|
||||||
Delete all objects (entities) with name `searchname`. To delete dropped items of a specific name, use `--item` followed by the name of the item. To delete *all* dropped items, exclude the `--item` flag and instead use the keyword `__builtin:item` (with two underscores) as the search name.
|
### `replacenodes`
|
||||||
|
|
||||||
|
**Usage:** `replacenodes [--p1 x y z] [--p2 x y z] [--invert] <searchnode> <replacenode>`
|
||||||
|
|
||||||
|
Replace all of one node with another node. Can be used to swap out a node that changed names or was deleted.
|
||||||
|
This command does not currently affect param2, node metadata, etc.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
|
||||||
|
- **`searchnode`**: Name of node to search for.
|
||||||
|
- **`replacenode`**: Name of node to replace with.
|
||||||
|
- **`--p1, --p2`**: Area in which to replace nodes. If not specified, nodes will be replaced across the entire map.
|
||||||
|
- **`--invert`**: Only replace nodes *outside* the given area.
|
||||||
|
|
||||||
|
### `setparam2`
|
||||||
|
|
||||||
|
**Usage:** `setparam2 [--searchnode <searchnode>] [--p1 x y z] [--p2 x y z] [--invert] <paramval>`
|
||||||
|
|
||||||
|
Set param2 values of a certain node and/or within a certain area.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
|
||||||
|
- **`paramval`**: Param2 value to set, between 0 and 255.
|
||||||
|
- **`--searchnode`**: Name of node to search for. If not specified, the param2 of all nodes will be set.
|
||||||
|
- **`--p1, --p2`**: Area in which to set param2. Required if `searchnode` is not specified.
|
||||||
|
- **`--invert`**: Only set param2 *outside* the given area.
|
||||||
|
|
||||||
|
### `deletemeta`
|
||||||
|
|
||||||
|
**Usage:** `deletemeta [--searchnode <searchnode>] [--p1 x y z] [--p2 x y z] [--invert]`
|
||||||
|
|
||||||
|
Delete metadata of a certain node and/or within a certain area. This includes node inventories as well.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
|
||||||
|
- **`--searchnode`**: Name of node to search for. If not specified, the metadata of all nodes will be deleted.
|
||||||
|
- **`--p1, --p2`**: Area in which to delete metadata. Required if `searchnode` is not specified.
|
||||||
|
- **`--invert`**: Only delete metadata *outside* the given area.
|
||||||
|
|
||||||
|
### `setmetavar`
|
||||||
|
|
||||||
|
**Usage:** `setmetavar [--searchnode <searchnode>] [--p1 x y z] [--p2 x y z] [--invert] <metakey> <metavalue>`
|
||||||
|
|
||||||
|
Set a variable in node metadata. This only works on metadata where the variable is already set.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
|
||||||
|
- **`metakey`**: Name of variable to set, e.g. `infotext`, `formspec`, etc.
|
||||||
|
- **`metavalue`**: Value to set variable to. This should be a string.
|
||||||
|
- **`--searchnode`**: Name of node to search for. If not specified, the variable will be set for all nodes that have it.
|
||||||
|
- **`--p1, --p2`**: Area in which to search. Required if `searchnode` is not specified.
|
||||||
|
- **`--invert`**: Only search for nodes *outside* the given area.
|
||||||
|
|
||||||
|
### `replaceininv`
|
||||||
|
|
||||||
|
**Usage:** ` replaceininv [--deletemeta] [--searchnode <searchnode>] [--p1 x y z] [--p2 x y z] [--invert] <searchitem> <replaceitem>`
|
||||||
|
|
||||||
|
Replace a certain item with another in node inventories.
|
||||||
|
To delete items instead of replacing them, use "Empty" (with a capital E) for `replacename`.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
|
||||||
|
- **`searchitem`**: Item to search for in node inventories.
|
||||||
|
- **`replaceitem`**: Item to replace with in node inventories.
|
||||||
|
- **`--deletemeta`**: Delete metadata of replaced items. If not specified, any item metadata will remain unchanged.
|
||||||
|
- **`--searchnode`**: Name of node to to replace in. If not specified, the item will be replaced in all node inventories.
|
||||||
|
- **`--p1, --p2`**: Area in which to search for nodes. If not specified, items will be replaced across the entire map.
|
||||||
|
- **`--invert`**: Only search for nodes *outside* the given area.
|
||||||
|
|
||||||
|
**Tip:** To only delete metadata without replacing the nodes, use the `--deletemeta` flag, and make `replaceitem` the same as `searchitem`.
|
||||||
|
|
||||||
|
### `deletetimers`
|
||||||
|
|
||||||
|
**Usage:** `deletetimers [--searchnode <searchnode>] [--p1 x y z] [--p2 x y z] [--invert]`
|
||||||
|
|
||||||
|
Delete node timers of a certain node and/or within a certain area.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
|
||||||
|
- **`--searchnode`**: Name of node to search for. If not specified, the node timers of all nodes will be deleted.
|
||||||
|
- **`--p1, --p2`**: Area in which to delete node timers. Required if `searchnode` is not specified.
|
||||||
|
- **`--invert`**: Only delete node timers *outside* the given area.
|
||||||
|
|
||||||
|
### `deleteobjects`
|
||||||
|
|
||||||
|
**Usage:** `deleteobjects [--searchobj <searchobj>] [--items] [--p1 x y z] [--p2 x y z] [--invert]`
|
||||||
|
|
||||||
|
Delete static objects of a certain name and/or within a certain area.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
|
||||||
|
- **`--searchobj`**: Name of object to search for, e.g. "boats:boat". If not specified, all objects will be deleted.
|
||||||
|
- **`--items`**: Search for only item entities (dropped items). `searchobj` determines the item name, if specified.
|
||||||
|
- **`--p1, --p2`**: Area in which to delete objects. If not specified, objects will be deleted across the entire map.
|
||||||
|
- **`--invert`**: Only delete objects *outside* the given area.
|
||||||
|
|
||||||
## Acknowledgments
|
## Acknowledgments
|
||||||
|
|
||||||
|
@ -1,27 +1,113 @@
|
|||||||
|
import numpy as np
|
||||||
import struct
|
import struct
|
||||||
|
from . import utils
|
||||||
|
|
||||||
def deserialize_metadata_vars(blob, numVars, version):
|
|
||||||
|
def clean_nimap(nimap, nodeData):
|
||||||
|
"""Removes unused or duplicate name-id mappings."""
|
||||||
|
for nid, name in utils.SafeEnum(nimap):
|
||||||
|
delete = False
|
||||||
|
firstOccur = nimap.index(name)
|
||||||
|
|
||||||
|
if firstOccur < nid:
|
||||||
|
# Name is a duplicate, since we are iterating backwards.
|
||||||
|
nodeData[nodeData == nid] = firstOccur
|
||||||
|
delete = True
|
||||||
|
|
||||||
|
if delete or np.all(nodeData != nid):
|
||||||
|
del nimap[nid]
|
||||||
|
nodeData[nodeData > nid] -= 1
|
||||||
|
|
||||||
|
|
||||||
|
class MapblockMerge:
|
||||||
|
"""Used to layer multiple mapblock fragments onto another block."""
|
||||||
|
def __init__(self, base):
|
||||||
|
self.base = base
|
||||||
|
self.layers = []
|
||||||
|
self.fromAreas = []
|
||||||
|
self.toAreas = []
|
||||||
|
|
||||||
|
def add_layer(self, mapBlock, fromArea, toArea):
|
||||||
|
self.layers.append(mapBlock)
|
||||||
|
self.fromAreas.append(fromArea)
|
||||||
|
self.toAreas.append(toArea)
|
||||||
|
|
||||||
|
def merge(self):
|
||||||
|
(baseND, baseParam1, baseParam2) = self.base.deserialize_node_data()
|
||||||
|
baseNimap = self.base.deserialize_nimap()
|
||||||
|
baseMetadata = self.base.deserialize_metadata()
|
||||||
|
baseTimers = self.base.deserialize_node_timers()
|
||||||
|
|
||||||
|
for i, layer in enumerate(self.layers):
|
||||||
|
fromArea = self.fromAreas[i]
|
||||||
|
toArea = self.toAreas[i]
|
||||||
|
fromSlices = fromArea.to_array_slices()
|
||||||
|
toSlices = toArea.to_array_slices()
|
||||||
|
|
||||||
|
(layerND, layerParam1, layerParam2) = layer.deserialize_node_data()
|
||||||
|
layerNimap = layer.deserialize_nimap()
|
||||||
|
|
||||||
|
layerND += len(baseNimap)
|
||||||
|
baseNimap.extend(layerNimap)
|
||||||
|
|
||||||
|
baseND[toSlices] = layerND[fromSlices]
|
||||||
|
baseParam1[toSlices] = layerParam1[fromSlices]
|
||||||
|
baseParam2[toSlices] = layerParam2[fromSlices]
|
||||||
|
|
||||||
|
areaOffset = toArea.p1 - fromArea.p1
|
||||||
|
|
||||||
|
for mIdx, meta in utils.SafeEnum(baseMetadata):
|
||||||
|
pos = utils.Vec3.from_u16_key(meta["pos"])
|
||||||
|
if toArea.contains(pos):
|
||||||
|
del baseMetadata[mIdx]
|
||||||
|
|
||||||
|
layerMetadata = layer.deserialize_metadata()
|
||||||
|
for meta in layerMetadata:
|
||||||
|
pos = utils.Vec3.from_u16_key(meta["pos"])
|
||||||
|
if fromArea.contains(pos):
|
||||||
|
meta["pos"] = (pos + areaOffset).to_u16_key()
|
||||||
|
baseMetadata.append(meta)
|
||||||
|
|
||||||
|
for tIdx, timer in utils.SafeEnum(baseTimers):
|
||||||
|
pos = utils.Vec3.from_u16_key(timer["pos"])
|
||||||
|
if toArea.contains(pos):
|
||||||
|
del baseTimers[tIdx]
|
||||||
|
|
||||||
|
# Clean up duplicate and unused name-id mappings
|
||||||
|
clean_nimap(baseNimap, baseND)
|
||||||
|
|
||||||
|
self.base.serialize_node_data(baseND, baseParam1, baseParam2)
|
||||||
|
self.base.serialize_nimap(baseNimap)
|
||||||
|
self.base.serialize_metadata(baseMetadata)
|
||||||
|
self.base.serialize_node_timers(baseTimers)
|
||||||
|
|
||||||
|
return self.base
|
||||||
|
|
||||||
|
|
||||||
|
def deserialize_metadata_vars(blob, count, metaVersion):
|
||||||
varList = {}
|
varList = {}
|
||||||
c = 0
|
c = 0
|
||||||
|
|
||||||
for i in range(numVars):
|
for i in range(count):
|
||||||
strLen = struct.unpack(">H", blob[c:c+2])[0]
|
strLen = struct.unpack(">H", blob[c:c+2])[0]
|
||||||
key = blob[c+2:c+2+strLen]
|
key = blob[c+2:c+2+strLen]
|
||||||
c += 2 + strLen
|
c += 2 + strLen
|
||||||
strLen = struct.unpack(">I", blob[c:c+4])[0]
|
strLen = struct.unpack(">I", blob[c:c+4])[0]
|
||||||
value = blob[c+4:c+4+strLen]
|
value = blob[c+4:c+4+strLen]
|
||||||
c += 4 + strLen
|
c += 4 + strLen
|
||||||
# Account for extra "is private" variable.
|
|
||||||
if version >= 2:
|
|
||||||
private = blob[c:c+1]
|
|
||||||
c += 1
|
|
||||||
|
|
||||||
varList[key] = [value, private]
|
if metaVersion >= 2:
|
||||||
|
isPrivate = blob[c]
|
||||||
|
c += 1
|
||||||
|
else:
|
||||||
|
isPrivate = 0
|
||||||
|
|
||||||
|
varList[key] = (value, isPrivate)
|
||||||
|
|
||||||
return varList
|
return varList
|
||||||
|
|
||||||
|
|
||||||
def serialize_metadata_vars(varList, version):
|
def serialize_metadata_vars(varList, metaVersion):
|
||||||
blob = b""
|
blob = b""
|
||||||
|
|
||||||
for key, data in varList.items():
|
for key, data in varList.items():
|
||||||
@ -29,7 +115,9 @@ def serialize_metadata_vars(varList, version):
|
|||||||
blob += key
|
blob += key
|
||||||
blob += struct.pack(">I", len(data[0]))
|
blob += struct.pack(">I", len(data[0]))
|
||||||
blob += data[0]
|
blob += data[0]
|
||||||
if version >= 2: blob += data[1]
|
|
||||||
|
if metaVersion >= 2:
|
||||||
|
blob += struct.pack("B", data[1])
|
||||||
|
|
||||||
return blob
|
return blob
|
||||||
|
|
||||||
|
1057
lib/commands.py
1057
lib/commands.py
File diff suppressed because it is too large
Load Diff
205
lib/helpers.py
205
lib/helpers.py
@ -1,205 +0,0 @@
|
|||||||
import sqlite3
|
|
||||||
import math
|
|
||||||
import time
|
|
||||||
import sys
|
|
||||||
|
|
||||||
class DatabaseHandler:
|
|
||||||
"""Handles the Sqlite database and provides useful functions."""
|
|
||||||
|
|
||||||
def __init__(self, filename, type):
|
|
||||||
if not filename:
|
|
||||||
throw_error("Please specify a map file ({:s}).".format(type))
|
|
||||||
|
|
||||||
try:
|
|
||||||
tempFile = open(filename, 'r')
|
|
||||||
tempFile.close()
|
|
||||||
except:
|
|
||||||
throw_error("Map file does not exist ({:s}).".format(type))
|
|
||||||
|
|
||||||
self.database = sqlite3.connect(filename)
|
|
||||||
self.cursor = self.database.cursor()
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.cursor.execute("SELECT * FROM blocks")
|
|
||||||
except sqlite3.DatabaseError:
|
|
||||||
throw_error("File is not a valid map file ({:s}).".format(type))
|
|
||||||
|
|
||||||
|
|
||||||
def get_block(self, pos):
|
|
||||||
self.cursor.execute("SELECT data FROM blocks WHERE pos = ?", (pos,))
|
|
||||||
return self.cursor.fetchone()[0]
|
|
||||||
|
|
||||||
|
|
||||||
def delete_block(self, pos):
|
|
||||||
self.cursor.execute("DELETE FROM blocks WHERE pos = ?", (pos,))
|
|
||||||
|
|
||||||
|
|
||||||
def set_block(self, pos, data, force = False):
|
|
||||||
if force:
|
|
||||||
self.cursor.execute(
|
|
||||||
"INSERT OR REPLACE INTO blocks (pos, data) VALUES (?, ?)",
|
|
||||||
(pos, data))
|
|
||||||
else:
|
|
||||||
self.cursor.execute("UPDATE blocks SET data = ? WHERE pos = ?",
|
|
||||||
(data, pos))
|
|
||||||
|
|
||||||
|
|
||||||
def close(self, commit = False):
|
|
||||||
if commit:
|
|
||||||
self.database.commit()
|
|
||||||
|
|
||||||
self.database.close()
|
|
||||||
|
|
||||||
|
|
||||||
class Progress:
|
|
||||||
"""Prints a progress bar with time elapsed."""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.last_total = 0
|
|
||||||
self.start_time = time.time()
|
|
||||||
|
|
||||||
|
|
||||||
def __del__(self):
|
|
||||||
self.print_bar(self.last_total, self.last_total)
|
|
||||||
|
|
||||||
|
|
||||||
def print_bar(self, completed, total):
|
|
||||||
self.last_total = total
|
|
||||||
|
|
||||||
if completed % 100 == 0 or completed == total:
|
|
||||||
if total > 0:
|
|
||||||
percent = round(completed / total * 100, 1)
|
|
||||||
else:
|
|
||||||
percent = 100
|
|
||||||
|
|
||||||
progress = math.floor(percent/2)
|
|
||||||
hours, remainder = divmod(int(time.time() - self.start_time), 3600)
|
|
||||||
minutes, seconds = divmod(remainder, 60)
|
|
||||||
|
|
||||||
print("|" + ('=' * progress) + (' ' * (50 - progress)) + "| " +
|
|
||||||
str(percent) + "% completed (" + str(completed) + "/" +
|
|
||||||
str(total) + " mapblocks) Elapsed: " +
|
|
||||||
"{:0>2}:{:0>2}:{:0>2}".format(hours, minutes, seconds),
|
|
||||||
end='\r')
|
|
||||||
|
|
||||||
|
|
||||||
class safeEnum:
|
|
||||||
"""Enumerates backwards over a list. This prevents items from being skipped
|
|
||||||
when deleting them."""
|
|
||||||
|
|
||||||
def __init__(self, list):
|
|
||||||
self.list = list
|
|
||||||
self.max = len(list)
|
|
||||||
|
|
||||||
|
|
||||||
def __iter__(self):
|
|
||||||
self.n = self.max
|
|
||||||
return self
|
|
||||||
|
|
||||||
|
|
||||||
def __next__(self):
|
|
||||||
if self.n > 0:
|
|
||||||
self.n -= 1
|
|
||||||
return self.n, self.list[self.n]
|
|
||||||
else:
|
|
||||||
raise StopIteration
|
|
||||||
|
|
||||||
|
|
||||||
def unsigned_to_signed(num, max_positive):
|
|
||||||
if num < max_positive:
|
|
||||||
return num
|
|
||||||
|
|
||||||
return num - (max_positive * 2)
|
|
||||||
|
|
||||||
|
|
||||||
def unhash_pos(num):
|
|
||||||
pos = [0, 0, 0]
|
|
||||||
|
|
||||||
pos[0] = unsigned_to_signed(num % 4096, 2048) # x value
|
|
||||||
num = (num - pos[0]) >> 12
|
|
||||||
pos[1] = unsigned_to_signed(num % 4096, 2048) # y value
|
|
||||||
num = (num - pos[1]) >> 12
|
|
||||||
pos[2] = unsigned_to_signed(num % 4096, 2048) # z value
|
|
||||||
|
|
||||||
return pos
|
|
||||||
|
|
||||||
|
|
||||||
def hash_pos(pos):
|
|
||||||
return (pos[0] +
|
|
||||||
pos[1] * 0x1000 +
|
|
||||||
pos[2] * 0x1000000)
|
|
||||||
|
|
||||||
|
|
||||||
def is_in_range(num, area):
|
|
||||||
p1, p2 = area[0], area[1]
|
|
||||||
|
|
||||||
x = unsigned_to_signed(num % 4096, 2048)
|
|
||||||
if x < p1[0] or x > p2[0]:
|
|
||||||
return False
|
|
||||||
|
|
||||||
num = (num - x) >> 12
|
|
||||||
y = unsigned_to_signed(num % 4096, 2048)
|
|
||||||
if y < p1[1] or y > p2[1]:
|
|
||||||
return False
|
|
||||||
|
|
||||||
num = (num - y) >> 12
|
|
||||||
z = unsigned_to_signed(num % 4096, 2048)
|
|
||||||
if z < p1[2] or z > p2[2]:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def get_mapblocks(database, area = None, name = None, inverse = False):
|
|
||||||
batch = []
|
|
||||||
list = []
|
|
||||||
|
|
||||||
while True:
|
|
||||||
batch = database.cursor.fetchmany(1000)
|
|
||||||
# Exit if we run out of database entries.
|
|
||||||
if len(batch) == 0:
|
|
||||||
break
|
|
||||||
|
|
||||||
for pos, data in batch:
|
|
||||||
# If an area is specified, check if it is in the area.
|
|
||||||
if area and is_in_range(pos, area) == inverse:
|
|
||||||
continue
|
|
||||||
# If a node name is specified, check if the name is in the data.
|
|
||||||
if name and data.find(name) < 0:
|
|
||||||
continue
|
|
||||||
# If checks pass, append item.
|
|
||||||
list.append(pos)
|
|
||||||
|
|
||||||
print("Building index, please wait... " + str(len(list)) +
|
|
||||||
" mapblocks found.", end="\r")
|
|
||||||
|
|
||||||
print("\nPerforming operation on about " + str(len(list)) + " mapblocks.")
|
|
||||||
return list
|
|
||||||
|
|
||||||
|
|
||||||
def args_to_mapblocks(p1, p2):
|
|
||||||
for i in range(3):
|
|
||||||
# Swap values so p1's values are always greater.
|
|
||||||
if p2[i] < p1[i]:
|
|
||||||
temp = p1[i]
|
|
||||||
p1[i] = p2[i]
|
|
||||||
p2[i] = temp
|
|
||||||
|
|
||||||
# Convert to mapblock coordinates
|
|
||||||
p1 = [math.ceil(n/16) for n in p1]
|
|
||||||
p2 = [math.floor((n + 1)/16) - 1 for n in p2]
|
|
||||||
|
|
||||||
return p1, p2
|
|
||||||
|
|
||||||
|
|
||||||
def verify_file(filename, msg):
|
|
||||||
try:
|
|
||||||
tempFile = open(filename, 'r')
|
|
||||||
tempFile.close()
|
|
||||||
except:
|
|
||||||
throw_error(msg)
|
|
||||||
|
|
||||||
|
|
||||||
def throw_error(msg):
|
|
||||||
print("ERROR: " + msg)
|
|
||||||
sys.exit()
|
|
203
lib/mapblock.py
203
lib/mapblock.py
@ -1,16 +1,41 @@
|
|||||||
import struct
|
import numpy as np
|
||||||
import zlib
|
import zlib
|
||||||
|
import struct
|
||||||
|
from . import utils
|
||||||
|
|
||||||
class MapBlock:
|
MIN_BLOCK_VER = 25
|
||||||
"""Stores a parsed version of a mapblock."""
|
MAX_BLOCK_VER = 28
|
||||||
|
|
||||||
|
|
||||||
|
def is_valid_generated(blob):
|
||||||
|
"""Returns true if a raw mapblock is valid and fully generated."""
|
||||||
|
return (blob and
|
||||||
|
len(blob) > 2 and
|
||||||
|
MIN_BLOCK_VER <= blob[0] <= MAX_BLOCK_VER and
|
||||||
|
blob[1] & 0x08 == 0)
|
||||||
|
|
||||||
|
|
||||||
|
class MapblockParseError(Exception):
|
||||||
|
"""Error parsing mapblock."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Mapblock:
|
||||||
|
"""Stores a parsed version of a mapblock.
|
||||||
|
|
||||||
|
For the Minetest C++ implementation, see the serialize/deserialize
|
||||||
|
methods in minetest/src/mapblock.cpp, as well as the related
|
||||||
|
functions called by those methods.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, blob):
|
def __init__(self, blob):
|
||||||
self.version = struct.unpack("B", blob[0:1])[0]
|
self.version = blob[0]
|
||||||
|
|
||||||
if self.version < 25 or self.version > 28:
|
if self.version < MIN_BLOCK_VER or self.version > MAX_BLOCK_VER:
|
||||||
return
|
raise MapblockParseError(
|
||||||
|
f"Unsupported mapblock version: {self.version}")
|
||||||
|
|
||||||
self.flags = blob[1:2]
|
self.flags = blob[1]
|
||||||
|
|
||||||
if self.version >= 27:
|
if self.version >= 27:
|
||||||
self.lighting_complete = blob[2:4]
|
self.lighting_complete = blob[2:4]
|
||||||
@ -19,16 +44,16 @@ class MapBlock:
|
|||||||
self.lighting_complete = 0xFFFF
|
self.lighting_complete = 0xFFFF
|
||||||
c = 2
|
c = 2
|
||||||
|
|
||||||
self.content_width = struct.unpack("B", blob[c:c+1])[0]
|
self.content_width = blob[c]
|
||||||
self.params_width = struct.unpack("B", blob[c+1:c+2])[0]
|
self.params_width = blob[c+1]
|
||||||
|
|
||||||
if self.content_width != 2 or self.params_width != 2:
|
if self.content_width != 2 or self.params_width != 2:
|
||||||
return
|
raise MapblockParseError("Unsupported content and/or param width")
|
||||||
|
|
||||||
# Decompress node data. This stores a node type id, param1 and param2
|
# Decompress node data. This stores a node type id, param1 and param2
|
||||||
# for each node.
|
# for each node.
|
||||||
decompresser = zlib.decompressobj()
|
decompresser = zlib.decompressobj()
|
||||||
self.node_data = decompresser.decompress(blob[c+2:])
|
self.node_data_raw = decompresser.decompress(blob[c+2:])
|
||||||
c = len(blob) - len(decompresser.unused_data)
|
c = len(blob) - len(decompresser.unused_data)
|
||||||
|
|
||||||
# Decompress node metadata.
|
# Decompress node metadata.
|
||||||
@ -37,7 +62,7 @@ class MapBlock:
|
|||||||
c = len(blob) - len(decompresser.unused_data)
|
c = len(blob) - len(decompresser.unused_data)
|
||||||
|
|
||||||
# Parse static objects.
|
# Parse static objects.
|
||||||
self.static_object_version = struct.unpack("B", blob[c:c+1])[0]
|
self.static_object_version = blob[c]
|
||||||
self.static_object_count = struct.unpack(">H", blob[c+1:c+3])[0]
|
self.static_object_count = struct.unpack(">H", blob[c+1:c+3])[0]
|
||||||
c += 3
|
c += 3
|
||||||
c2 = c
|
c2 = c
|
||||||
@ -55,7 +80,11 @@ class MapBlock:
|
|||||||
self.timestamp = struct.unpack(">I", blob[c:c+4])[0]
|
self.timestamp = struct.unpack(">I", blob[c:c+4])[0]
|
||||||
|
|
||||||
# Parse name-id mappings.
|
# Parse name-id mappings.
|
||||||
self.nimap_version = struct.unpack("B", blob[c+4:c+5])[0]
|
self.nimap_version = blob[c+4]
|
||||||
|
if self.nimap_version != 0:
|
||||||
|
raise MapblockParseError(
|
||||||
|
f"Unsupported nimap version: {self.nimap_version}")
|
||||||
|
|
||||||
self.nimap_count = struct.unpack(">H", blob[c+5:c+7])[0]
|
self.nimap_count = struct.unpack(">H", blob[c+5:c+7])[0]
|
||||||
c += 7
|
c += 7
|
||||||
c2 = c
|
c2 = c
|
||||||
@ -70,42 +99,56 @@ class MapBlock:
|
|||||||
self.nimap_raw = blob[c:c2]
|
self.nimap_raw = blob[c:c2]
|
||||||
c = c2
|
c = c2
|
||||||
|
|
||||||
# Get raw node timers.
|
# Get raw node timers. Includes version and count.
|
||||||
self.node_timers_count = struct.unpack(">H", blob[c+1:c+3])[0]
|
self.node_timers_raw = blob[c:]
|
||||||
self.node_timers_raw = blob[c+3:]
|
|
||||||
|
|
||||||
|
|
||||||
def serialize(self):
|
def serialize(self):
|
||||||
blob = b""
|
blob = b""
|
||||||
|
|
||||||
blob += struct.pack("B", self.version)
|
blob += struct.pack("BB", self.version, self.flags)
|
||||||
blob += self.flags
|
|
||||||
|
|
||||||
if self.version >= 27:
|
if self.version >= 27:
|
||||||
blob += self.lighting_complete
|
blob += self.lighting_complete
|
||||||
|
|
||||||
blob += struct.pack("B", self.content_width)
|
blob += struct.pack("BB", self.content_width, self.params_width)
|
||||||
blob += struct.pack("B", self.params_width)
|
|
||||||
|
|
||||||
blob += zlib.compress(self.node_data)
|
blob += zlib.compress(self.node_data_raw)
|
||||||
blob += zlib.compress(self.node_metadata)
|
blob += zlib.compress(self.node_metadata)
|
||||||
|
|
||||||
blob += struct.pack("B", self.static_object_version)
|
blob += struct.pack(">BH",
|
||||||
blob += struct.pack(">H", self.static_object_count)
|
self.static_object_version, self.static_object_count)
|
||||||
blob += self.static_objects_raw
|
blob += self.static_objects_raw
|
||||||
|
|
||||||
blob += struct.pack(">I", self.timestamp)
|
blob += struct.pack(">I", self.timestamp)
|
||||||
|
|
||||||
blob += struct.pack("B", self.nimap_version)
|
blob += struct.pack(">BH", self.nimap_version, self.nimap_count)
|
||||||
blob += struct.pack(">H", self.nimap_count)
|
|
||||||
blob += self.nimap_raw
|
blob += self.nimap_raw
|
||||||
|
|
||||||
blob += b"\x0A" # The timer data length is basically unused.
|
|
||||||
blob += struct.pack(">H", self.node_timers_count)
|
|
||||||
blob += self.node_timers_raw
|
blob += self.node_timers_raw
|
||||||
|
|
||||||
return blob
|
return blob
|
||||||
|
|
||||||
|
def get_raw_content(self, idx):
|
||||||
|
"""Get the raw 2-byte ID of a node at a given index."""
|
||||||
|
return self.node_data_raw[idx * self.content_width :
|
||||||
|
(idx + 1) * self.content_width]
|
||||||
|
|
||||||
|
def deserialize_node_data(self):
|
||||||
|
nodeData = np.frombuffer(self.node_data_raw,
|
||||||
|
count=4096, dtype=">u2")
|
||||||
|
param1 = np.frombuffer(self.node_data_raw,
|
||||||
|
offset=8192, count=4096, dtype="u1")
|
||||||
|
param2 = np.frombuffer(self.node_data_raw,
|
||||||
|
offset=12288, count=4096, dtype="u1")
|
||||||
|
|
||||||
|
return tuple(np.reshape(arr, (16, 16, 16)).copy()
|
||||||
|
for arr in (nodeData, param1, param2))
|
||||||
|
|
||||||
|
def serialize_node_data(self, nodeData, param1, param2):
|
||||||
|
self.node_data_raw = (
|
||||||
|
nodeData.tobytes() +
|
||||||
|
param1.tobytes() +
|
||||||
|
param2.tobytes()
|
||||||
|
)
|
||||||
|
|
||||||
def deserialize_nimap(self):
|
def deserialize_nimap(self):
|
||||||
nimapList = [None] * self.nimap_count
|
nimapList = [None] * self.nimap_count
|
||||||
@ -113,53 +156,49 @@ class MapBlock:
|
|||||||
|
|
||||||
for i in range(self.nimap_count):
|
for i in range(self.nimap_count):
|
||||||
# Parse node id and node name length.
|
# Parse node id and node name length.
|
||||||
id = struct.unpack(">H", self.nimap_raw[c:c+2])[0]
|
(nid, strSize) = struct.unpack(">HH", self.nimap_raw[c:c+4])
|
||||||
strSize = struct.unpack(">H", self.nimap_raw[c+2:c+4])[0]
|
|
||||||
# Parse node name
|
# Parse node name
|
||||||
c += 4
|
c += 4
|
||||||
name = self.nimap_raw[c:c+strSize]
|
name = self.nimap_raw[c:c+strSize]
|
||||||
c += strSize
|
c += strSize
|
||||||
|
|
||||||
nimapList[id] = name
|
nimapList[nid] = name
|
||||||
|
|
||||||
return nimapList
|
return nimapList
|
||||||
|
|
||||||
|
|
||||||
def serialize_nimap(self, nimapList):
|
def serialize_nimap(self, nimapList):
|
||||||
blob = b""
|
blob = b""
|
||||||
|
|
||||||
for i in range(len(nimapList)):
|
for nid in range(len(nimapList)):
|
||||||
blob += struct.pack(">H", i)
|
blob += struct.pack(">HH", nid, len(nimapList[nid]))
|
||||||
blob += struct.pack(">H", len(nimapList[i]))
|
blob += nimapList[nid]
|
||||||
blob += nimapList[i]
|
|
||||||
|
|
||||||
self.nimap_count = len(nimapList)
|
self.nimap_count = len(nimapList)
|
||||||
self.nimap_raw = blob
|
self.nimap_raw = blob
|
||||||
|
|
||||||
|
|
||||||
def deserialize_metadata(self):
|
def deserialize_metadata(self):
|
||||||
metaList = []
|
metaList = []
|
||||||
self.metadata_version = struct.unpack("B", self.node_metadata[0:1])[0]
|
self.metadata_version = self.node_metadata[0]
|
||||||
|
|
||||||
# A version number of 0 indicates no metadata is present.
|
# A version number of 0 indicates no metadata is present.
|
||||||
if self.metadata_version == 0:
|
if self.metadata_version == 0:
|
||||||
return metaList
|
return metaList
|
||||||
elif self.metadata_version > 2:
|
elif self.metadata_version > 2:
|
||||||
helpers.throw_error("ERROR: Metadata version not supported.")
|
raise MapblockParseError(
|
||||||
|
f"Unsupported metadata version: {self.metadata_version}")
|
||||||
|
|
||||||
count = struct.unpack(">H", self.node_metadata[1:3])[0]
|
count = struct.unpack(">H", self.node_metadata[1:3])[0]
|
||||||
c = 3
|
c = 3
|
||||||
|
|
||||||
for i in range(count):
|
for i in range(count):
|
||||||
metaList.append({})
|
meta = {}
|
||||||
metaList[i]["pos"] = struct.unpack(">H",
|
|
||||||
self.node_metadata[c:c+2])[0]
|
(meta["pos"], meta["numVars"]) = struct.unpack(">HI",
|
||||||
metaList[i]["numVars"] = struct.unpack(">I",
|
self.node_metadata[c:c+6])
|
||||||
self.node_metadata[c+2:c+6])[0]
|
|
||||||
c += 6
|
c += 6
|
||||||
c2 = c
|
c2 = c
|
||||||
|
|
||||||
for a in range(metaList[i]["numVars"]):
|
for a in range(meta["numVars"]):
|
||||||
strLen = struct.unpack(">H", self.node_metadata[c2:c2+2])[0]
|
strLen = struct.unpack(">H", self.node_metadata[c2:c2+2])[0]
|
||||||
c2 += 2 + strLen
|
c2 += 2 + strLen
|
||||||
strLen = struct.unpack(">I", self.node_metadata[c2:c2+4])[0]
|
strLen = struct.unpack(">I", self.node_metadata[c2:c2+4])[0]
|
||||||
@ -167,14 +206,15 @@ class MapBlock:
|
|||||||
# Account for extra "is private" variable.
|
# Account for extra "is private" variable.
|
||||||
c2 += 1 if self.metadata_version >= 2 else 0
|
c2 += 1 if self.metadata_version >= 2 else 0
|
||||||
|
|
||||||
metaList[i]["vars"] = self.node_metadata[c:c2]
|
meta["vars"] = self.node_metadata[c:c2]
|
||||||
c = c2
|
c = c2
|
||||||
c2 = self.node_metadata.find(b"EndInventory\n", c) + 13
|
c2 = self.node_metadata.find(b"EndInventory\n", c) + 13
|
||||||
metaList[i]["inv"] = self.node_metadata[c:c2]
|
meta["inv"] = self.node_metadata[c:c2]
|
||||||
c = c2
|
c = c2
|
||||||
|
|
||||||
return metaList
|
metaList.append(meta)
|
||||||
|
|
||||||
|
return metaList
|
||||||
|
|
||||||
def serialize_metadata(self, metaList):
|
def serialize_metadata(self, metaList):
|
||||||
blob = b""
|
blob = b""
|
||||||
@ -183,70 +223,91 @@ class MapBlock:
|
|||||||
self.node_metadata = b"\x00"
|
self.node_metadata = b"\x00"
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
|
# Metadata version is just determined from the block version.
|
||||||
|
self.metadata_version = 2 if self.version > 27 else 1
|
||||||
blob += struct.pack("B", self.metadata_version)
|
blob += struct.pack("B", self.metadata_version)
|
||||||
|
|
||||||
blob += struct.pack(">H", len(metaList))
|
blob += struct.pack(">H", len(metaList))
|
||||||
|
|
||||||
for meta in metaList:
|
for meta in metaList:
|
||||||
blob += struct.pack(">H", meta["pos"])
|
blob += struct.pack(">HI", meta["pos"], meta["numVars"])
|
||||||
blob += struct.pack(">I", meta["numVars"])
|
|
||||||
blob += meta["vars"]
|
blob += meta["vars"]
|
||||||
blob += meta["inv"]
|
blob += meta["inv"]
|
||||||
|
|
||||||
self.node_metadata = blob
|
self.node_metadata = blob
|
||||||
|
|
||||||
|
|
||||||
def deserialize_static_objects(self):
|
def deserialize_static_objects(self):
|
||||||
objectList = []
|
objectList = []
|
||||||
c = 0
|
c = 0
|
||||||
|
|
||||||
for i in range(self.static_object_count):
|
for i in range(self.static_object_count):
|
||||||
type = struct.unpack("B", self.static_objects_raw[c:c+1])[0]
|
objType = self.static_objects_raw[c]
|
||||||
pos = self.static_objects_raw[c+1:c+13]
|
pos = self.static_objects_raw[c+1:c+13]
|
||||||
strLen = struct.unpack(">H", self.static_objects_raw[c+13:c+15])[0]
|
strLen = struct.unpack(">H", self.static_objects_raw[c+13:c+15])[0]
|
||||||
c += 15
|
c += 15
|
||||||
data = self.static_objects_raw[c:c+strLen]
|
data = self.static_objects_raw[c:c+strLen]
|
||||||
c += strLen
|
c += strLen
|
||||||
objectList.append({"type": type, "pos": pos, "data": data})
|
objectList.append({"type": objType, "pos": pos, "data": data})
|
||||||
|
|
||||||
return objectList
|
return objectList
|
||||||
|
|
||||||
|
|
||||||
def serialize_static_objects(self, objectList):
|
def serialize_static_objects(self, objectList):
|
||||||
blob = b""
|
blob = b""
|
||||||
|
|
||||||
for object in objectList:
|
for sObject in objectList:
|
||||||
blob += struct.pack("B", object["type"])
|
blob += struct.pack("B", sObject["type"])
|
||||||
blob += object["pos"]
|
blob += sObject["pos"]
|
||||||
blob += struct.pack(">H", len(object["data"]))
|
blob += struct.pack(">H", len(sObject["data"]))
|
||||||
blob += object["data"]
|
blob += sObject["data"]
|
||||||
|
|
||||||
self.static_objects_raw = blob
|
self.static_objects_raw = blob
|
||||||
self.static_object_count = len(objectList)
|
self.static_object_count = len(objectList)
|
||||||
|
|
||||||
|
|
||||||
def deserialize_node_timers(self):
|
def deserialize_node_timers(self):
|
||||||
timerList = []
|
timerList = []
|
||||||
c = 0
|
|
||||||
|
|
||||||
for i in range(self.node_timers_count):
|
# The first byte changed from version to data length, for some reason.
|
||||||
pos = struct.unpack(">H", self.node_timers_raw[c:c+2])[0]
|
if self.version == 24:
|
||||||
timeout = struct.unpack(">I", self.node_timers_raw[c+2:c+6])[0]
|
version = self.node_timers_raw[0]
|
||||||
elapsed = struct.unpack(">I", self.node_timers_raw[c+6:c+10])[0]
|
if version == 0:
|
||||||
|
return timerList
|
||||||
|
elif version != 1:
|
||||||
|
raise MapblockParseError(
|
||||||
|
f"Unsupported node timer version: {version}")
|
||||||
|
elif self.version >= 25:
|
||||||
|
datalen = self.node_timers_raw[0]
|
||||||
|
if datalen != 10:
|
||||||
|
raise MapblockParseError(
|
||||||
|
f"Unsupported node timer data length: {datalen}")
|
||||||
|
|
||||||
|
count = struct.unpack(">H", self.node_timers_raw[1:3])[0]
|
||||||
|
c = 3
|
||||||
|
|
||||||
|
for i in range(count):
|
||||||
|
(pos, timeout, elapsed) = struct.unpack(">HII",
|
||||||
|
self.node_timers_raw[c:c+10])
|
||||||
c += 10
|
c += 10
|
||||||
timerList.append({"pos": pos, "timeout": timeout,
|
timerList.append({"pos": pos, "timeout": timeout,
|
||||||
"elapsed": elapsed})
|
"elapsed": elapsed})
|
||||||
|
|
||||||
return timerList
|
return timerList
|
||||||
|
|
||||||
|
|
||||||
def serialize_node_timers(self, timerList):
|
def serialize_node_timers(self, timerList):
|
||||||
blob = b""
|
blob = b""
|
||||||
|
count = len(timerList)
|
||||||
|
|
||||||
|
if self.version == 24:
|
||||||
|
if count == 0:
|
||||||
|
blob += b"\x00"
|
||||||
|
else:
|
||||||
|
blob += b"\x01"
|
||||||
|
blob += struct.pack(">H", count)
|
||||||
|
elif self.version >= 25:
|
||||||
|
blob += b"\x0A"
|
||||||
|
blob += struct.pack(">H", count)
|
||||||
|
|
||||||
for i, timer in enumerate(timerList):
|
for i, timer in enumerate(timerList):
|
||||||
blob += struct.pack(">H", timer["pos"])
|
blob += struct.pack(">HII",
|
||||||
blob += struct.pack(">I", timer["timeout"])
|
timer["pos"], timer["timeout"], timer["elapsed"])
|
||||||
blob += struct.pack(">I", timer["elapsed"])
|
|
||||||
|
|
||||||
self.node_timers_raw = blob
|
self.node_timers_raw = blob
|
||||||
self.node_timers_count = len(timerList)
|
|
||||||
|
305
lib/utils.py
Normal file
305
lib/utils.py
Normal file
@ -0,0 +1,305 @@
|
|||||||
|
import sqlite3
|
||||||
|
from typing import NamedTuple
|
||||||
|
import struct
|
||||||
|
import math
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
class Vec3(NamedTuple):
|
||||||
|
"""Vector to store 3D coordinates."""
|
||||||
|
x: int = 0
|
||||||
|
y: int = 0
|
||||||
|
z: int = 0
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_block_key(cls, key):
|
||||||
|
(key, x) = divmod(key + 0x800, 0x1000)
|
||||||
|
(z, y) = divmod(key + 0x800, 0x1000)
|
||||||
|
return cls(x - 0x800, y - 0x800, z)
|
||||||
|
|
||||||
|
def to_block_key(self):
|
||||||
|
return (self.x * 0x1 +
|
||||||
|
self.y * 0x1000 +
|
||||||
|
self.z * 0x1000000)
|
||||||
|
|
||||||
|
def is_valid_block_pos(self):
|
||||||
|
"""Determines if a block position is valid and usable.
|
||||||
|
|
||||||
|
Block positions up to 2048 can still be converted to a
|
||||||
|
mapblock key, but Minetest only loads blocks within 31000
|
||||||
|
nodes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
limit = 31000 // 16
|
||||||
|
return (-limit <= self.x <= limit and
|
||||||
|
-limit <= self.y <= limit and
|
||||||
|
-limit <= self.z <= limit)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_u16_key(cls, key):
|
||||||
|
return cls(key % 16,
|
||||||
|
(key >> 4) % 16,
|
||||||
|
(key >> 8) % 16)
|
||||||
|
|
||||||
|
def to_u16_key(self):
|
||||||
|
return self.x + self.y * 16 + self.z * 256
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_v3f1000(cls, pos):
|
||||||
|
# *10 accounts for block size, so it's not really 1000x.
|
||||||
|
fac = 1000.0 * 10
|
||||||
|
(x, y, z) = struct.unpack(">iii", pos)
|
||||||
|
return cls(x / fac, y / fac, z / fac)
|
||||||
|
|
||||||
|
def map(self, func):
|
||||||
|
return Vec3(*(func(n) for n in self))
|
||||||
|
|
||||||
|
def __add__(self, other):
|
||||||
|
return Vec3(self.x + other.x,
|
||||||
|
self.y + other.y,
|
||||||
|
self.z + other.z)
|
||||||
|
|
||||||
|
def __sub__(self, other):
|
||||||
|
return Vec3(self.x - other.x,
|
||||||
|
self.y - other.y,
|
||||||
|
self.z - other.z)
|
||||||
|
|
||||||
|
def __mul__(self, other):
|
||||||
|
if type(other) == Vec3:
|
||||||
|
return Vec3(self.x * other.x,
|
||||||
|
self.y * other.y,
|
||||||
|
self.z * other.z)
|
||||||
|
elif type(other) == int:
|
||||||
|
return Vec3(*(n * other for n in self))
|
||||||
|
else:
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
|
||||||
|
class Area(NamedTuple):
|
||||||
|
"""Area defined by two corner Vec3's.
|
||||||
|
|
||||||
|
All of p1's coordinates must be less than or equal to p2's.
|
||||||
|
"""
|
||||||
|
|
||||||
|
p1: Vec3
|
||||||
|
p2: Vec3
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_args(cls, p1, p2):
|
||||||
|
pMin = Vec3(min(p1[0], p2[0]), min(p1[1], p2[1]), min(p1[2], p2[2]))
|
||||||
|
pMax = Vec3(max(p1[0], p2[0]), max(p1[1], p2[1]), max(p1[2], p2[2]))
|
||||||
|
return cls(pMin, pMax)
|
||||||
|
|
||||||
|
def to_array_slices(self):
|
||||||
|
"""Convert area to tuple of slices for NumPy array indexing."""
|
||||||
|
return (slice(self.p1.z, self.p2.z + 1),
|
||||||
|
slice(self.p1.y, self.p2.y + 1),
|
||||||
|
slice(self.p1.x, self.p2.x + 1))
|
||||||
|
|
||||||
|
def contains(self, pos):
|
||||||
|
return (self.p1.x <= pos.x <= self.p2.x and
|
||||||
|
self.p1.y <= pos.y <= self.p2.y and
|
||||||
|
self.p1.z <= pos.z <= self.p2.z)
|
||||||
|
|
||||||
|
def is_full_mapblock(self):
|
||||||
|
return self.p1 == Vec3(0, 0, 0) and self.p2 == Vec3(15, 15, 15)
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
for x in range(self.p1.x, self.p2.x + 1):
|
||||||
|
for y in range(self.p1.y, self.p2.y + 1):
|
||||||
|
for z in range(self.p1.z, self.p2.z + 1):
|
||||||
|
yield Vec3(x, y, z)
|
||||||
|
|
||||||
|
def __add__(self, offset):
|
||||||
|
return Area(self.p1 + offset, self.p2 + offset)
|
||||||
|
|
||||||
|
def __sub__(self, offset):
|
||||||
|
return Area(self.p1 - offset, self.p2 - offset)
|
||||||
|
|
||||||
|
|
||||||
|
def get_block_overlap(blockPos, area, relative=False):
|
||||||
|
cornerPos = blockPos * 16
|
||||||
|
relArea = area - cornerPos
|
||||||
|
relOverlap = Area(
|
||||||
|
relArea.p1.map(lambda n: max(n, 0)),
|
||||||
|
relArea.p2.map(lambda n: min(n, 15))
|
||||||
|
)
|
||||||
|
|
||||||
|
if (relOverlap.p1.x > relOverlap.p2.x or
|
||||||
|
relOverlap.p1.y > relOverlap.p2.y or
|
||||||
|
relOverlap.p1.z > relOverlap.p2.z):
|
||||||
|
# p1 is greater than p2, meaning there is no overlap.
|
||||||
|
return None
|
||||||
|
|
||||||
|
if relative:
|
||||||
|
return relOverlap
|
||||||
|
else:
|
||||||
|
return relOverlap + cornerPos
|
||||||
|
|
||||||
|
|
||||||
|
def get_overlap_slice(blockPos, area):
|
||||||
|
return get_block_overlap(blockPos, area, relative=True).to_array_slices()
|
||||||
|
|
||||||
|
|
||||||
|
class DatabaseHandler:
|
||||||
|
"""Handles an SQLite database and provides useful methods."""
|
||||||
|
|
||||||
|
def __init__(self, filename):
|
||||||
|
try:
|
||||||
|
open(filename, 'r').close()
|
||||||
|
except FileNotFoundError:
|
||||||
|
raise
|
||||||
|
|
||||||
|
self.database = sqlite3.connect(filename)
|
||||||
|
self.cursor = self.database.cursor()
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.cursor.execute("SELECT pos, data FROM blocks")
|
||||||
|
except sqlite3.DatabaseError:
|
||||||
|
raise
|
||||||
|
|
||||||
|
def get_block(self, key):
|
||||||
|
self.cursor.execute("SELECT data FROM blocks WHERE pos = ?", (key,))
|
||||||
|
if data := self.cursor.fetchone():
|
||||||
|
return data[0]
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_many(self, num):
|
||||||
|
return self.cursor.fetchmany(num)
|
||||||
|
|
||||||
|
def delete_block(self, key):
|
||||||
|
self.cursor.execute("DELETE FROM blocks WHERE pos = ?", (key,))
|
||||||
|
|
||||||
|
def set_block(self, key, data, force=False):
|
||||||
|
# TODO: Remove force?
|
||||||
|
if force:
|
||||||
|
self.cursor.execute(
|
||||||
|
"INSERT OR REPLACE INTO blocks (pos, data) VALUES (?, ?)",
|
||||||
|
(key, data))
|
||||||
|
else:
|
||||||
|
self.cursor.execute("UPDATE blocks SET data = ? WHERE pos = ?",
|
||||||
|
(data, key))
|
||||||
|
|
||||||
|
def is_modified(self):
|
||||||
|
return self.database.in_transaction
|
||||||
|
|
||||||
|
def close(self, commit=False):
|
||||||
|
if self.is_modified() and commit:
|
||||||
|
self.database.commit()
|
||||||
|
|
||||||
|
self.database.close()
|
||||||
|
|
||||||
|
|
||||||
|
def get_mapblock_area(area, invert=False, includePartial=False):
|
||||||
|
"""Get "positive" area.
|
||||||
|
|
||||||
|
If the area is inverted, only mapblocks outside this area should be
|
||||||
|
modified.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if invert == includePartial:
|
||||||
|
# Partial mapblocks are excluded.
|
||||||
|
return Area(area.p1.map(lambda n: (n + 15) // 16),
|
||||||
|
area.p2.map(lambda n: (n - 15) // 16))
|
||||||
|
else:
|
||||||
|
# Partial mapblocks are included.
|
||||||
|
return Area(area.p1.map(lambda n: n // 16),
|
||||||
|
area.p2.map(lambda n: n // 16))
|
||||||
|
|
||||||
|
|
||||||
|
def get_mapblocks(database, searchData=None, area=None, invert=False,
|
||||||
|
includePartial=False):
|
||||||
|
"""Returns a list of all mapblocks that fit the given criteria."""
|
||||||
|
keys = []
|
||||||
|
|
||||||
|
if area:
|
||||||
|
blockArea = get_mapblock_area(area, invert=invert,
|
||||||
|
includePartial=includePartial)
|
||||||
|
else:
|
||||||
|
blockArea = None
|
||||||
|
|
||||||
|
while True:
|
||||||
|
batch = database.get_many(1000)
|
||||||
|
# Exit if we run out of database entries.
|
||||||
|
if len(batch) == 0:
|
||||||
|
break
|
||||||
|
|
||||||
|
for key, data in batch:
|
||||||
|
# Make sure the block is inside/outside the area as specified.
|
||||||
|
if (blockArea and
|
||||||
|
blockArea.contains(Vec3.from_block_key(key)) == invert):
|
||||||
|
continue
|
||||||
|
# Specifies a node name or other string to search for.
|
||||||
|
if searchData and data.find(searchData) == -1:
|
||||||
|
continue
|
||||||
|
# If checks pass, add the key to the list.
|
||||||
|
keys.append(key)
|
||||||
|
|
||||||
|
print(f"\rBuilding index... {len(keys)} mapblocks found.", end="")
|
||||||
|
|
||||||
|
print()
|
||||||
|
return keys
|
||||||
|
|
||||||
|
|
||||||
|
class Progress:
|
||||||
|
"""Prints a progress bar with time elapsed."""
|
||||||
|
PRINT_INTERVAL = 0.25
|
||||||
|
BAR_LEN = 50
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.start_time = None
|
||||||
|
self.last_total = 0
|
||||||
|
self.last_time = 0
|
||||||
|
|
||||||
|
def _print_bar(self, completed, total, timeNow):
|
||||||
|
fProgress = completed / total if total > 0 else 1.0
|
||||||
|
numBars = math.floor(fProgress * self.BAR_LEN)
|
||||||
|
percent = fProgress * 100
|
||||||
|
|
||||||
|
remMinutes, seconds = divmod(int(timeNow - self.start_time), 60)
|
||||||
|
hours, minutes = divmod(remMinutes, 60)
|
||||||
|
|
||||||
|
print(f"\r|{'=' * numBars}{' ' * (self.BAR_LEN - numBars)}| "
|
||||||
|
f"{percent:.1f}% completed ({completed}/{total} mapblocks) "
|
||||||
|
f"{hours:0>2}:{minutes:0>2}:{seconds:0>2}",
|
||||||
|
end="")
|
||||||
|
|
||||||
|
self.last_time = timeNow
|
||||||
|
|
||||||
|
def set_start(self):
|
||||||
|
self.start_time = time.time()
|
||||||
|
|
||||||
|
def update_bar(self, completed, total):
|
||||||
|
self.last_total = total
|
||||||
|
timeNow = time.time()
|
||||||
|
|
||||||
|
if timeNow - self.last_time > self.PRINT_INTERVAL:
|
||||||
|
self._print_bar(completed, total, timeNow)
|
||||||
|
|
||||||
|
def update_final(self):
|
||||||
|
if self.start_time:
|
||||||
|
self._print_bar(self.last_total, self.last_total, time.time())
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
class SafeEnum:
|
||||||
|
"""Enumerates backwards over a list.
|
||||||
|
|
||||||
|
This prevents items from being skipped when deleting them.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, iterable):
|
||||||
|
self.iterable = iterable
|
||||||
|
self.max = len(iterable)
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
self.n = self.max
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __next__(self):
|
||||||
|
if self.n > 0:
|
||||||
|
self.n -= 1
|
||||||
|
return self.n, self.iterable[self.n]
|
||||||
|
else:
|
||||||
|
raise StopIteration
|
311
mapedit.py
311
mapedit.py
@ -1,182 +1,163 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
import argparse
|
import argparse
|
||||||
import sys
|
from lib import commands
|
||||||
import re
|
# TODO: Fix file structure, add setuptools?
|
||||||
from lib import commands, helpers
|
|
||||||
|
|
||||||
inputFile = ""
|
ARGUMENT_DEFS = {
|
||||||
outputFile = ""
|
"p1": {
|
||||||
|
"always_opt": True,
|
||||||
|
"params": {
|
||||||
|
"type": int,
|
||||||
|
"nargs": 3,
|
||||||
|
"metavar": ("x", "y", "z"),
|
||||||
|
"help": "Corner position 1 of area",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"p2": {
|
||||||
|
"always_opt": True,
|
||||||
|
"params": {
|
||||||
|
"type": int,
|
||||||
|
"nargs": 3,
|
||||||
|
"metavar": ("x", "y", "z"),
|
||||||
|
"help": "Corner position 2 of area",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"invert": {
|
||||||
|
"params": {
|
||||||
|
"action": "store_true",
|
||||||
|
"help": "Select everything OUTSIDE the given area."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"blockmode": {
|
||||||
|
"params": {
|
||||||
|
"action": "store_true",
|
||||||
|
"help": "Work on whole mapblocks instead of node regions. "
|
||||||
|
"May be considerably faster in some cases."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"offset": {
|
||||||
|
"always_opt": True,
|
||||||
|
"params": {
|
||||||
|
"type": int,
|
||||||
|
"nargs": 3,
|
||||||
|
"metavar": ("x", "y", "z"),
|
||||||
|
"help": "Vector to move area by",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"searchnode": {
|
||||||
|
"params": {
|
||||||
|
"metavar": "<searchnode>",
|
||||||
|
"help": "Name of node to search for"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"replacenode": {
|
||||||
|
"params": {
|
||||||
|
"metavar": "<replacenode>",
|
||||||
|
"help": "Name of node to replace with"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"searchitem": {
|
||||||
|
"params": {
|
||||||
|
"metavar": "<searchitem>",
|
||||||
|
"help": "Name of item to search for"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"replaceitem": {
|
||||||
|
"params": {
|
||||||
|
"metavar": "<replaceitem>",
|
||||||
|
"help": "Name of item to replace with"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"metakey": {
|
||||||
|
"params": {
|
||||||
|
"metavar": "<metakey>",
|
||||||
|
"help": "Name of variable to set"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"metavalue": {
|
||||||
|
"params": {
|
||||||
|
"metavar": "<metavalue>",
|
||||||
|
"help": "Value to set variable to"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"searchobj": {
|
||||||
|
"params": {
|
||||||
|
"metavar": "<searchobj>",
|
||||||
|
"help": "Name of object to search for"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"paramval": {
|
||||||
|
"params": {
|
||||||
|
"type": int,
|
||||||
|
"metavar": "<paramval>",
|
||||||
|
"help": "Value to set param2 to."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"input_file": {
|
||||||
|
"params": {
|
||||||
|
"metavar": "<input_file>",
|
||||||
|
"help": "Path to secondary (input) map file"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"deletemeta": {
|
||||||
|
"params": {
|
||||||
|
"action": "store_true",
|
||||||
|
"help": "Delete item metadata when replacing items."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"items": {
|
||||||
|
"params": {
|
||||||
|
"action": "store_true",
|
||||||
|
"help": "Search for item entities (dropped items)."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# Initialize parsers.
|
||||||
|
|
||||||
# Parse arguments
|
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description="Edit Minetest map and player database files.")
|
description="Edit Minetest map database files.",
|
||||||
|
epilog="Run `mapedit.py <command> -h` for command-specific help.")
|
||||||
|
|
||||||
parser.add_argument("-f",
|
parser.add_argument("-f",
|
||||||
required=True,
|
required=True,
|
||||||
|
dest="file",
|
||||||
metavar="<file>",
|
metavar="<file>",
|
||||||
help="Path to primary map file")
|
help="Path to primary map file")
|
||||||
parser.add_argument("-s",
|
parser.add_argument("--no-warnings",
|
||||||
required=False,
|
dest="no_warnings",
|
||||||
metavar="<file>",
|
|
||||||
help="Path to secondary (input) map file")
|
|
||||||
|
|
||||||
parser.add_argument("--p1",
|
|
||||||
type=int,
|
|
||||||
nargs=3,
|
|
||||||
metavar=("x", "y", "z"),
|
|
||||||
help="Position 1 (specified in nodes)")
|
|
||||||
parser.add_argument("--p2",
|
|
||||||
type=int,
|
|
||||||
nargs=3,
|
|
||||||
metavar=("x", "y", "z"),
|
|
||||||
help="Position 2 (specified in nodes)")
|
|
||||||
parser.add_argument("--inverse",
|
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="Select all mapblocks NOT in the given area.")
|
help="Don't show warnings or confirmation prompts.")
|
||||||
|
|
||||||
parser.add_argument("--silencewarnings",
|
subparsers = parser.add_subparsers(dest="command", required=True,
|
||||||
action="store_true")
|
|
||||||
|
|
||||||
subparsers = parser.add_subparsers(dest="command",
|
|
||||||
help="Command (see README.md for more information)")
|
help="Command (see README.md for more information)")
|
||||||
|
|
||||||
# Initialize basic mapblock-based commands.
|
for cmdName, cmdDef in commands.COMMAND_DEFS.items():
|
||||||
parser_cloneblocks = subparsers.add_parser("cloneblocks",
|
subparser = subparsers.add_parser(cmdName, help=cmdDef["help"])
|
||||||
help="Clone the given area to a new location on the map.")
|
|
||||||
parser_cloneblocks.set_defaults(func=commands.clone_blocks)
|
|
||||||
|
|
||||||
parser_deleteblocks = subparsers.add_parser("deleteblocks",
|
for arg, required in cmdDef["args"].items():
|
||||||
help="Delete all mapblocks in the given area.")
|
argsToAdd = ("p1", "p2") if arg == "area" else (arg,)
|
||||||
parser_deleteblocks.set_defaults(func=commands.delete_blocks)
|
|
||||||
|
|
||||||
parser_fillblocks = subparsers.add_parser("fillblocks",
|
for argToAdd in argsToAdd:
|
||||||
help="Fill the given area with a certain type of node.")
|
argDef = ARGUMENT_DEFS[argToAdd]
|
||||||
parser_fillblocks.set_defaults(func=commands.fill_blocks)
|
|
||||||
|
|
||||||
parser_overlayblocks = subparsers.add_parser("overlayblocks",
|
if "always_opt" in argDef and argDef["always_opt"]:
|
||||||
help="Overlay any mapblocks from secondary file into given area.")
|
# Always use an option flag, even if not required.
|
||||||
parser_overlayblocks.set_defaults(func=commands.overlay_blocks)
|
subparser.add_argument("--" + argToAdd, required=required,
|
||||||
|
**argDef["params"])
|
||||||
|
else:
|
||||||
|
if required:
|
||||||
|
subparser.add_argument(argToAdd, **argDef["params"])
|
||||||
|
else:
|
||||||
|
subparser.add_argument("--" + argToAdd, required=False,
|
||||||
|
**argDef["params"])
|
||||||
|
|
||||||
parser_cloneblocks.add_argument("--offset",
|
# Handle the actual command.
|
||||||
required=True,
|
|
||||||
type=int,
|
|
||||||
nargs=3,
|
|
||||||
metavar=("x", "y", "z"),
|
|
||||||
help="Vector to move area by (specified in nodes)")
|
|
||||||
parser_fillblocks.add_argument("replacename",
|
|
||||||
metavar="<name>",
|
|
||||||
help="Name of node to fill area with")
|
|
||||||
|
|
||||||
# Initialize node-based commands.
|
args = commands.MapEditArgs()
|
||||||
parser_replacenodes = subparsers.add_parser("replacenodes",
|
parser.parse_args(namespace=args)
|
||||||
help="Replace all of one type of node with another.")
|
inst = commands.MapEditInstance()
|
||||||
parser_replacenodes.set_defaults(func=commands.replace_nodes)
|
inst.run(args)
|
||||||
|
|
||||||
parser_setparam2 = subparsers.add_parser("setparam2",
|
|
||||||
help="Set param2 values of all of a certain type of node.")
|
|
||||||
parser_setparam2.set_defaults(func=commands.set_param2)
|
|
||||||
|
|
||||||
parser_deletemeta = subparsers.add_parser("deletemeta",
|
|
||||||
help="Delete metadata of all of a certain type of node.")
|
|
||||||
parser_deletemeta.set_defaults(func=commands.delete_meta)
|
|
||||||
|
|
||||||
parser_setmetavar = subparsers.add_parser("setmetavar",
|
|
||||||
help="Set a value in the metadata of all of a certain type of node.")
|
|
||||||
parser_setmetavar.set_defaults(func=commands.set_meta_var)
|
|
||||||
|
|
||||||
parser_replaceininv = subparsers.add_parser("replaceininv",
|
|
||||||
help="Replace one item with another in inventories certain nodes.")
|
|
||||||
parser_replaceininv.set_defaults(func=commands.replace_in_inv)
|
|
||||||
|
|
||||||
parser_deletetimers = subparsers.add_parser("deletetimers",
|
|
||||||
help="Delete node timers of all of a certain type of node.")
|
|
||||||
parser_deletetimers.set_defaults(func=commands.delete_timers)
|
|
||||||
|
|
||||||
for command in (parser_replacenodes, parser_setparam2, parser_deletemeta,
|
|
||||||
parser_setmetavar, parser_replaceininv, parser_deletetimers):
|
|
||||||
command.add_argument("searchname",
|
|
||||||
metavar="<searchname>",
|
|
||||||
help="Name of node to search for")
|
|
||||||
|
|
||||||
parser_replacenodes.add_argument("replacename",
|
|
||||||
metavar="<replacename>",
|
|
||||||
help="Name of node to replace with")
|
|
||||||
parser_setparam2.add_argument("value",
|
|
||||||
type=int,
|
|
||||||
metavar="<value>",
|
|
||||||
help="Param2 value to replace with (0 for non-directional nodes)")
|
|
||||||
parser_setmetavar.add_argument("key",
|
|
||||||
metavar="<key>",
|
|
||||||
help="Name of variable to set")
|
|
||||||
parser_setmetavar.add_argument("value",
|
|
||||||
metavar="<value>",
|
|
||||||
help="Value to set variable to")
|
|
||||||
parser_replaceininv.add_argument("searchitem",
|
|
||||||
metavar="<searchitem>",
|
|
||||||
help="Name of item to search for")
|
|
||||||
parser_replaceininv.add_argument("replaceitem",
|
|
||||||
metavar="<replaceitem>",
|
|
||||||
help="Name of item to replace with")
|
|
||||||
parser_replaceininv.add_argument("--deletemeta",
|
|
||||||
action="store_true",
|
|
||||||
help="Delete item metadata when replacing items.")
|
|
||||||
|
|
||||||
# Initialize miscellaneous commands.
|
|
||||||
parser_deleteobjects = subparsers.add_parser("deleteobjects",
|
|
||||||
help="Delete all objects with the specified name.")
|
|
||||||
parser_deleteobjects.set_defaults(func=commands.delete_objects)
|
|
||||||
|
|
||||||
parser_deleteobjects.add_argument("--item",
|
|
||||||
action="store_true",
|
|
||||||
help="Search for item entities (dropped items).")
|
|
||||||
parser_deleteobjects.add_argument("searchname",
|
|
||||||
metavar="<searchname>",
|
|
||||||
help="Name of object to search for")
|
|
||||||
|
|
||||||
# Begin handling the command.
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
if not args.command:
|
|
||||||
helpers.throw_error("No command specified.")
|
|
||||||
|
|
||||||
# Verify area coordinates.
|
|
||||||
if args.command in ("cloneblocks", "deleteblocks", "fillblocks",
|
|
||||||
"overlayblocks"):
|
|
||||||
if not args.p1 or not args.p2:
|
|
||||||
helpers.throw_error("Command requires --p1 and --p2 arguments.")
|
|
||||||
|
|
||||||
# Verify any node/item names.
|
|
||||||
nameFormat = re.compile("^[a-zA-Z0-9_]+:[a-zA-Z0-9_]+$")
|
|
||||||
|
|
||||||
for param in ("searchname", "replacename", "searchitem", "replaceitem"):
|
|
||||||
if hasattr(args, param):
|
|
||||||
value = getattr(args, param)
|
|
||||||
|
|
||||||
if (nameFormat.match(value) == None and value != "air" and not
|
|
||||||
(param == "replaceitem" and value == "Empty")):
|
|
||||||
helpers.throw_error("Invalid node name ({:s}).".format(param))
|
|
||||||
|
|
||||||
# Attempt to open database.
|
|
||||||
db = helpers.DatabaseHandler(args.f, "primary")
|
|
||||||
|
|
||||||
if not args.silencewarnings and input(
|
|
||||||
"WARNING: Using this tool can potentially cause permanent\n"
|
|
||||||
"damage to your map database. Please SHUT DOWN the game/server\n"
|
|
||||||
"and BACK UP the map before proceeding. To continue this\n"
|
|
||||||
"operation, type 'yes'.\n"
|
|
||||||
"> ") != "yes":
|
|
||||||
sys.exit()
|
|
||||||
|
|
||||||
if args.command == "overlayblocks":
|
|
||||||
if args.s == args.f:
|
|
||||||
helpers.throw_error("Primary and secondary map files are the same.")
|
|
||||||
|
|
||||||
sDb = helpers.DatabaseHandler(args.s, "secondary")
|
|
||||||
|
|
||||||
args.func(db, sDb, args)
|
|
||||||
sDb.close()
|
|
||||||
else:
|
|
||||||
args.func(db, args)
|
|
||||||
|
|
||||||
print("\nSaving file...")
|
|
||||||
db.close(commit=True)
|
|
||||||
|
|
||||||
print("Done.")
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user