mirror of
https://github.com/luanti-org/luanti.git
synced 2025-09-15 18:57:08 +00:00
Merge branch 'luanti-org:master' into master
This commit is contained in:
commit
f3071dcd82
23 changed files with 499 additions and 341 deletions
|
@ -102,7 +102,7 @@ end
|
|||
local translation_file_header = [[
|
||||
// This file is automatically generated
|
||||
// It contains a bunch of fake gettext calls, to tell xgettext about the strings in config files
|
||||
// To update it, refer to the bottom of builtin/mainmenu/dlg_settings_advanced.lua
|
||||
// To update it, refer to the bottom of builtin/common/settings/init.lua
|
||||
|
||||
fake_function() {]]
|
||||
|
||||
|
@ -110,15 +110,15 @@ local function create_translation_file(settings)
|
|||
local result = { translation_file_header }
|
||||
for _, entry in ipairs(settings) do
|
||||
if entry.type == "category" then
|
||||
insert(result, sprintf("\tgettext(%q);", entry.name))
|
||||
insert(result, sprintf("\t/* xgettext:no-c-format */ gettext(%q);", entry.name))
|
||||
else
|
||||
if entry.readable_name then
|
||||
insert(result, sprintf("\tgettext(%q);", entry.readable_name))
|
||||
insert(result, sprintf("\t/* xgettext:no-c-format */ gettext(%q);", entry.readable_name))
|
||||
end
|
||||
if entry.comment ~= "" then
|
||||
local comment_escaped = entry.comment:gsub("\n", "\\n")
|
||||
comment_escaped = comment_escaped:gsub("\"", "\\\"")
|
||||
insert(result, "\tgettext(\"" .. comment_escaped .. "\");")
|
||||
insert(result, "\t/* xgettext:no-c-format */ gettext(\"" .. comment_escaped .. "\");")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5005,7 +5005,8 @@ A VoxelManip object can be created any time using either:
|
|||
If the optional position parameters are present for either of these routines,
|
||||
the specified region will be pre-loaded into the VoxelManip object on creation.
|
||||
Otherwise, the area of map you wish to manipulate must first be loaded into the
|
||||
VoxelManip object using `VoxelManip:read_from_map()`.
|
||||
VoxelManip object using `VoxelManip:read_from_map()`, or an empty one created
|
||||
with `VoxelManip:initialize()`.
|
||||
|
||||
Note that `VoxelManip:read_from_map()` returns two position vectors. The region
|
||||
formed by these positions indicate the minimum and maximum (respectively)
|
||||
|
@ -5016,14 +5017,14 @@ be queried any time after loading map data with `VoxelManip:get_emerged_area()`.
|
|||
Now that the VoxelManip object is populated with map data, your mod can fetch a
|
||||
copy of this data using either of two methods. `VoxelManip:get_node_at()`,
|
||||
which retrieves an individual node in a MapNode formatted table at the position
|
||||
requested is the simplest method to use, but also the slowest.
|
||||
requested. This is the simplest method to use, but also the slowest.
|
||||
|
||||
Nodes in a VoxelManip object may also be read in bulk to a flat array table
|
||||
using:
|
||||
|
||||
* `VoxelManip:get_data()` for node content (in Content ID form, see section
|
||||
[Content IDs]),
|
||||
* `VoxelManip:get_light_data()` for node light levels, and
|
||||
* `VoxelManip:get_light_data()` for node param (usually light levels), and
|
||||
* `VoxelManip:get_param2_data()` for the node type-dependent "param2" values.
|
||||
|
||||
See section [Flat array format] for more details.
|
||||
|
@ -5038,17 +5039,16 @@ internal state unless otherwise explicitly stated.
|
|||
Once the bulk data has been edited to your liking, the internal VoxelManip
|
||||
state can be set using:
|
||||
|
||||
* `VoxelManip:set_data()` for node content (in Content ID form, see section
|
||||
[Content IDs]),
|
||||
* `VoxelManip:set_light_data()` for node light levels, and
|
||||
* `VoxelManip:set_param2_data()` for the node type-dependent `param2` values.
|
||||
* `VoxelManip:set_data()` or
|
||||
* `VoxelManip:set_light_data()` or
|
||||
* `VoxelManip:set_param2_data()`
|
||||
|
||||
The parameter to each of the above three functions can use any table at all in
|
||||
the same flat array format as produced by `get_data()` etc. and is not required
|
||||
to be a table retrieved from `get_data()`.
|
||||
|
||||
Once the internal VoxelManip state has been modified to your liking, the
|
||||
changes can be committed back to the map by calling `VoxelManip:write_to_map()`
|
||||
changes can be committed back to the map by calling `VoxelManip:write_to_map()`.
|
||||
|
||||
### Flat array format
|
||||
|
||||
|
@ -5180,15 +5180,22 @@ inside the VoxelManip.
|
|||
Methods
|
||||
-------
|
||||
|
||||
* `read_from_map(p1, p2)`: Loads a chunk of map into the VoxelManip object
|
||||
* `read_from_map(p1, p2)`: Loads a part of the map into the VoxelManip object
|
||||
containing the region formed by `p1` and `p2`.
|
||||
* returns actual emerged `pmin`, actual emerged `pmax`
|
||||
* returns actual emerged `pmin`, actual emerged `pmax` (MapBlock-aligned)
|
||||
* Note that calling this multiple times will *add* to the area loaded in the
|
||||
VoxelManip, and not reset it.
|
||||
* `initialize(p1, p2, [node])`: Clears and resizes the VoxelManip object to
|
||||
comprise the region formed by `p1` and `p2`.
|
||||
* **No data** is read from the map, so you can use this to treat `VoxelManip`
|
||||
objects as general containers of node data.
|
||||
* `node`: if present the data will be filled with this node; if not it will
|
||||
be uninitialized
|
||||
* returns actual emerged `pmin`, actual emerged `pmax` (MapBlock-aligned)
|
||||
* (introduced in 5.13.0)
|
||||
* `write_to_map([light])`: Writes the data loaded from the `VoxelManip` back to
|
||||
the map.
|
||||
* **important**: data must be set using `VoxelManip:set_data()` before
|
||||
calling this.
|
||||
* **important**: you should call `set_data()` before this, or nothing will change.
|
||||
* if `light` is true, then lighting is automatically recalculated.
|
||||
The default value is true.
|
||||
If `light` is false, no light calculations happen, and you should correct
|
||||
|
@ -5249,6 +5256,15 @@ Methods
|
|||
where the engine will keep the map and the VM in sync automatically.
|
||||
* Note: this doesn't do what you think it does and is subject to removal. Don't use it!
|
||||
* `get_emerged_area()`: Returns actual emerged minimum and maximum positions.
|
||||
* "Emerged" does not imply that this region was actually loaded from the map,
|
||||
if `initialize()` has been used.
|
||||
* `close()`: Frees the data buffers associated with the VoxelManip object.
|
||||
It will become empty.
|
||||
* Since Lua's garbage collector is not aware of the potentially significant
|
||||
memory behind a VoxelManip, frequent VoxelManip usage can cause the server to
|
||||
run out of RAM. Therefore it's recommend to call this method once you're done
|
||||
with the VoxelManip.
|
||||
* (introduced in 5.13.0)
|
||||
|
||||
`VoxelArea`
|
||||
-----------
|
||||
|
|
|
@ -23,8 +23,8 @@ Callbacks
|
|||
* `core.button_handler(fields)`: called when a button is pressed.
|
||||
* `fields` = `{name1 = value1, name2 = value2, ...}`
|
||||
* `core.event_handler(event)`
|
||||
* `event`: `"MenuQuit"`, `"KeyEnter"`, `"ExitButton"`, `"EditBoxEnter"` or
|
||||
`"FullscreenChange"`
|
||||
* `event`: `"MenuQuit"` (derived from `quit`) or `"FullscreenChange"`
|
||||
The main menu may issue custom events, such as `"Refresh"` (server list).
|
||||
* `core.on_before_close()`: called before the menu is closed, either to exit or
|
||||
to join a game
|
||||
|
||||
|
|
|
@ -31,16 +31,6 @@ struct TextDestNodeMetadata : public TextDest
|
|||
m_p = p;
|
||||
m_client = client;
|
||||
}
|
||||
// This is deprecated I guess? -celeron55
|
||||
void gotText(const std::wstring &text)
|
||||
{
|
||||
std::string ntext = wide_to_utf8(text);
|
||||
infostream << "Submitting 'text' field of node at (" << m_p.X << ","
|
||||
<< m_p.Y << "," << m_p.Z << "): " << ntext << std::endl;
|
||||
StringMap fields;
|
||||
fields["text"] = ntext;
|
||||
m_client->sendNodemetaFields(m_p, "", fields);
|
||||
}
|
||||
void gotText(const StringMap &fields)
|
||||
{
|
||||
m_client->sendNodemetaFields(m_p, "", fields);
|
||||
|
|
|
@ -27,12 +27,13 @@ public:
|
|||
void fill(v3s16 bpmin, v3s16 bpmax, MapNode n)
|
||||
{
|
||||
for (s16 z = bpmin.Z; z <= bpmax.Z; z++)
|
||||
for (s16 y = bpmin.Y; y <= bpmax.Y; y++)
|
||||
for (s16 x = bpmin.X; x <= bpmax.X; x++) {
|
||||
for (s16 x = bpmin.X; x <= bpmax.X; x++)
|
||||
for (s16 y = bpmin.Y; y <= bpmax.Y; y++) {
|
||||
MapBlock *block = getBlockNoCreateNoEx({x, y, z});
|
||||
if (block) {
|
||||
auto *data = block->getData();
|
||||
for (size_t i = 0; i < MapBlock::nodecount; i++)
|
||||
block->getData()[i] = n;
|
||||
data[i] = n;
|
||||
block->expireIsAirCache();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,12 +40,6 @@ void TextDestGuiEngine::gotText(const StringMap &fields)
|
|||
m_engine->getScriptIface()->handleMainMenuButtons(fields);
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
void TextDestGuiEngine::gotText(const std::wstring &text)
|
||||
{
|
||||
m_engine->getScriptIface()->handleMainMenuEvent(wide_to_utf8(text));
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
MenuTextureSource::~MenuTextureSource()
|
||||
{
|
||||
|
|
|
@ -61,12 +61,6 @@ public:
|
|||
*/
|
||||
void gotText(const StringMap &fields);
|
||||
|
||||
/**
|
||||
* receive text/events transmitted by guiFormSpecMenu
|
||||
* @param text textual representation of event
|
||||
*/
|
||||
void gotText(const std::wstring &text);
|
||||
|
||||
private:
|
||||
/** target to transmit data to */
|
||||
GUIEngine *m_engine = nullptr;
|
||||
|
|
|
@ -77,6 +77,9 @@
|
|||
return; \
|
||||
}
|
||||
|
||||
// Element ID of the "Proceed" button shown for sizeless formspecs
|
||||
constexpr s32 ID_PROCEED_BTN = 257;
|
||||
|
||||
/*
|
||||
GUIFormSpecMenu
|
||||
*/
|
||||
|
@ -2998,7 +3001,7 @@ void GUIFormSpecMenu::regenerateGui(v2u32 screensize)
|
|||
gui::IGUIElement *focused_element = Environment->getFocus();
|
||||
if (focused_element && focused_element->getParent() == this) {
|
||||
s32 focused_id = focused_element->getID();
|
||||
if (focused_id > 257) {
|
||||
if (focused_id > ID_PROCEED_BTN) {
|
||||
for (const GUIFormSpecMenu::FieldSpec &field : m_fields) {
|
||||
if (field.fid == focused_id) {
|
||||
m_focused_element = field.fname;
|
||||
|
@ -3308,7 +3311,7 @@ void GUIFormSpecMenu::regenerateGui(v2u32 screensize)
|
|||
size.X / 2 - 70, pos.Y,
|
||||
size.X / 2 - 70 + 140, pos.Y + m_btn_height * 2
|
||||
);
|
||||
GUIButton::addButton(Environment, mydata.rect, m_tsrc, this, 257,
|
||||
GUIButton::addButton(Environment, mydata.rect, m_tsrc, this, ID_PROCEED_BTN,
|
||||
wstrgettext("Proceed").c_str());
|
||||
}
|
||||
}
|
||||
|
@ -4031,12 +4034,7 @@ bool GUIFormSpecMenu::preprocessEvent(const SEvent& event)
|
|||
if (m_joystick->wasKeyDown(KeyType::ESC)) {
|
||||
tryClose();
|
||||
} else if (m_joystick->wasKeyDown(KeyType::JUMP)) {
|
||||
if (m_allowclose) {
|
||||
acceptInput(quit_mode_accept);
|
||||
quitMenu();
|
||||
} else {
|
||||
acceptInput(quit_mode_try);
|
||||
}
|
||||
trySubmitClose();
|
||||
}
|
||||
}
|
||||
return handled;
|
||||
|
@ -4056,6 +4054,16 @@ void GUIFormSpecMenu::tryClose()
|
|||
}
|
||||
}
|
||||
|
||||
void GUIFormSpecMenu::trySubmitClose()
|
||||
{
|
||||
if (m_allowclose) {
|
||||
acceptInput(quit_mode_accept);
|
||||
quitMenu();
|
||||
} else {
|
||||
acceptInput(quit_mode_try);
|
||||
}
|
||||
}
|
||||
|
||||
bool GUIFormSpecMenu::OnEvent(const SEvent& event)
|
||||
{
|
||||
if (event.EventType==EET_KEY_INPUT_EVENT) {
|
||||
|
@ -4099,12 +4107,7 @@ bool GUIFormSpecMenu::OnEvent(const SEvent& event)
|
|||
break;
|
||||
}
|
||||
if (current_keys_pending.key_enter) {
|
||||
if (m_allowclose) {
|
||||
acceptInput(quit_mode_accept);
|
||||
quitMenu();
|
||||
} else {
|
||||
acceptInput(quit_mode_try);
|
||||
}
|
||||
trySubmitClose();
|
||||
} else {
|
||||
acceptInput();
|
||||
}
|
||||
|
@ -4818,12 +4821,18 @@ bool GUIFormSpecMenu::OnEvent(const SEvent& event)
|
|||
}
|
||||
|
||||
if (event.EventType == EET_GUI_EVENT) {
|
||||
if (event.GUIEvent.EventType == gui::EGET_TAB_CHANGED
|
||||
&& isVisible()) {
|
||||
// find the element that was clicked
|
||||
const s32 caller_id = event.GUIEvent.Caller->getID();
|
||||
bool close_on_enter;
|
||||
|
||||
switch (event.GUIEvent.EventType) {
|
||||
case gui::EGET_TAB_CHANGED:
|
||||
if (!isVisible())
|
||||
break;
|
||||
|
||||
// find the element that was clicked
|
||||
for (GUIFormSpecMenu::FieldSpec &s : m_fields) {
|
||||
if ((s.ftype == f_TabHeader) &&
|
||||
(s.fid == event.GUIEvent.Caller->getID())) {
|
||||
if (s.ftype == f_TabHeader &&
|
||||
s.fid == caller_id) {
|
||||
if (!s.sound.empty() && m_sound_manager)
|
||||
m_sound_manager->playSound(0, SoundSpec(s.sound, 1.0f));
|
||||
s.send = true;
|
||||
|
@ -4832,26 +4841,26 @@ bool GUIFormSpecMenu::OnEvent(const SEvent& event)
|
|||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (event.GUIEvent.EventType == gui::EGET_ELEMENT_FOCUS_LOST
|
||||
&& isVisible()) {
|
||||
break;
|
||||
|
||||
case gui::EGET_ELEMENT_FOCUS_LOST:
|
||||
if (!isVisible())
|
||||
break;
|
||||
|
||||
if (!canTakeFocus(event.GUIEvent.Element)) {
|
||||
infostream<<"GUIFormSpecMenu: Not allowing focus change."
|
||||
<<std::endl;
|
||||
// Returning true disables focus change
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if ((event.GUIEvent.EventType == gui::EGET_BUTTON_CLICKED) ||
|
||||
(event.GUIEvent.EventType == gui::EGET_CHECKBOX_CHANGED) ||
|
||||
(event.GUIEvent.EventType == gui::EGET_COMBO_BOX_CHANGED) ||
|
||||
(event.GUIEvent.EventType == gui::EGET_SCROLL_BAR_CHANGED)) {
|
||||
s32 caller_id = event.GUIEvent.Caller->getID();
|
||||
break;
|
||||
|
||||
if (caller_id == 257) {
|
||||
acceptInput(quit_mode_accept);
|
||||
m_text_dst->gotText(L"ExitButton");
|
||||
quitMenu();
|
||||
case gui::EGET_BUTTON_CLICKED:
|
||||
case gui::EGET_CHECKBOX_CHANGED:
|
||||
case gui::EGET_COMBO_BOX_CHANGED:
|
||||
case gui::EGET_SCROLL_BAR_CHANGED:
|
||||
if (caller_id == ID_PROCEED_BTN) {
|
||||
trySubmitClose();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -4881,7 +4890,6 @@ bool GUIFormSpecMenu::OnEvent(const SEvent& event)
|
|||
|
||||
if (s.is_exit) {
|
||||
acceptInput(quit_mode_accept);
|
||||
m_text_dst->gotText(L"ExitButton");
|
||||
quitMenu();
|
||||
return true;
|
||||
}
|
||||
|
@ -4922,60 +4930,57 @@ bool GUIFormSpecMenu::OnEvent(const SEvent& event)
|
|||
s.send = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (event.GUIEvent.EventType == gui::EGET_SCROLL_BAR_CHANGED) {
|
||||
// move scroll_containers
|
||||
for (const std::pair<std::string, GUIScrollContainer *> &c : m_scroll_containers)
|
||||
c.second->onScrollEvent(event.GUIEvent.Caller);
|
||||
}
|
||||
if (event.GUIEvent.EventType == gui::EGET_SCROLL_BAR_CHANGED) {
|
||||
// move scroll_containers
|
||||
for (const std::pair<std::string, GUIScrollContainer *> &c : m_scroll_containers)
|
||||
c.second->onScrollEvent(event.GUIEvent.Caller);
|
||||
}
|
||||
break;
|
||||
|
||||
if (event.GUIEvent.EventType == gui::EGET_EDITBOX_ENTER) {
|
||||
if (event.GUIEvent.Caller->getID() > 257) {
|
||||
bool close_on_enter = true;
|
||||
for (GUIFormSpecMenu::FieldSpec &s : m_fields) {
|
||||
if (s.ftype == f_Unknown &&
|
||||
s.fid == event.GUIEvent.Caller->getID()) {
|
||||
current_field_enter_pending = s.fname;
|
||||
auto it = field_close_on_enter.find(s.fname);
|
||||
if (it != field_close_on_enter.end())
|
||||
close_on_enter = (*it).second;
|
||||
case gui::EGET_EDITBOX_ENTER:
|
||||
if (caller_id <= ID_PROCEED_BTN)
|
||||
break;
|
||||
|
||||
break;
|
||||
}
|
||||
close_on_enter = true;
|
||||
for (GUIFormSpecMenu::FieldSpec &s : m_fields) {
|
||||
if (s.ftype == f_Unknown &&
|
||||
s.fid == caller_id) {
|
||||
current_field_enter_pending = s.fname;
|
||||
auto it = field_close_on_enter.find(s.fname);
|
||||
if (it != field_close_on_enter.end())
|
||||
close_on_enter = (*it).second;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
current_keys_pending.key_enter = true;
|
||||
current_keys_pending.key_enter = true;
|
||||
|
||||
if (close_on_enter) {
|
||||
if (m_allowclose) {
|
||||
acceptInput(quit_mode_accept);
|
||||
quitMenu();
|
||||
} else {
|
||||
acceptInput(quit_mode_try);
|
||||
}
|
||||
} else {
|
||||
if (close_on_enter)
|
||||
trySubmitClose();
|
||||
else
|
||||
acceptInput();
|
||||
return true;
|
||||
|
||||
case gui::EGET_TABLE_CHANGED:
|
||||
if (caller_id <= ID_PROCEED_BTN)
|
||||
break;
|
||||
|
||||
// find the element that was clicked
|
||||
for (GUIFormSpecMenu::FieldSpec &s : m_fields) {
|
||||
// if it's a table, set the send field
|
||||
// so lua knows which table was changed
|
||||
if (s.ftype == f_Table && s.fid == caller_id) {
|
||||
s.send = true;
|
||||
acceptInput();
|
||||
s.send = false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
||||
if (event.GUIEvent.EventType == gui::EGET_TABLE_CHANGED) {
|
||||
int current_id = event.GUIEvent.Caller->getID();
|
||||
if (current_id > 257) {
|
||||
// find the element that was clicked
|
||||
for (GUIFormSpecMenu::FieldSpec &s : m_fields) {
|
||||
// if it's a table, set the send field
|
||||
// so lua knows which table was changed
|
||||
if ((s.ftype == f_Table) && (s.fid == current_id)) {
|
||||
s.send = true;
|
||||
acceptInput();
|
||||
s.send=false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -68,8 +68,6 @@ struct TextDest
|
|||
{
|
||||
virtual ~TextDest() = default;
|
||||
|
||||
// This is deprecated I guess? -celeron55
|
||||
virtual void gotText(const std::wstring &text) {}
|
||||
virtual void gotText(const StringMap &fields) = 0;
|
||||
|
||||
std::string m_formname;
|
||||
|
@ -493,6 +491,7 @@ private:
|
|||
bool parseMiddleRect(const std::string &value, core::rect<s32> *parsed_rect);
|
||||
|
||||
void tryClose();
|
||||
void trySubmitClose();
|
||||
|
||||
void showTooltip(const std::wstring &text, const irr::video::SColor &color,
|
||||
const irr::video::SColor &bgcolor);
|
||||
|
|
108
src/map.cpp
108
src/map.cpp
|
@ -772,52 +772,79 @@ void MMVManip::initialEmerge(v3s16 p_min, v3s16 p_max, bool load_if_inexistent)
|
|||
infostream<<std::endl;
|
||||
}
|
||||
|
||||
const bool all_new = m_area.hasEmptyExtent() || block_area_nodes.contains(m_area);
|
||||
std::map<v3s16, bool> had_blocks;
|
||||
// we can skip this calculation if the areas are disjoint
|
||||
if (!m_area.intersect(block_area_nodes).hasEmptyExtent())
|
||||
had_blocks = getCoveredBlocks();
|
||||
|
||||
const bool all_new = m_area.hasEmptyExtent();
|
||||
addArea(block_area_nodes);
|
||||
|
||||
for(s32 z=p_min.Z; z<=p_max.Z; z++)
|
||||
for(s32 y=p_min.Y; y<=p_max.Y; y++)
|
||||
for(s32 x=p_min.X; x<=p_max.X; x++)
|
||||
{
|
||||
u8 flags = 0;
|
||||
MapBlock *block;
|
||||
v3s16 p(x,y,z);
|
||||
if (m_loaded_blocks.count(p) > 0)
|
||||
// if this block was already in the vmanip and it has data, skip
|
||||
if (auto it = had_blocks.find(p); it != had_blocks.end() && it->second)
|
||||
continue;
|
||||
|
||||
bool block_data_inexistent = false;
|
||||
{
|
||||
TimeTaker timer2("emerge load", &emerge_load_time);
|
||||
|
||||
block = m_map->getBlockNoCreateNoEx(p);
|
||||
if (!block)
|
||||
block_data_inexistent = true;
|
||||
else
|
||||
block->copyTo(*this);
|
||||
}
|
||||
|
||||
if(block_data_inexistent)
|
||||
{
|
||||
|
||||
MapBlock *block = m_map->getBlockNoCreateNoEx(p);
|
||||
if (block) {
|
||||
block->copyTo(*this);
|
||||
} else {
|
||||
if (load_if_inexistent && !blockpos_over_max_limit(p)) {
|
||||
block = m_map->emergeBlock(p, true);
|
||||
assert(block);
|
||||
block->copyTo(*this);
|
||||
} else {
|
||||
flags |= VMANIP_BLOCK_DATA_INEXIST;
|
||||
|
||||
// Mark area inexistent
|
||||
VoxelArea a(p*MAP_BLOCKSIZE, (p+1)*MAP_BLOCKSIZE-v3s16(1,1,1));
|
||||
setFlags(a, VOXELFLAG_NO_DATA);
|
||||
}
|
||||
}
|
||||
|
||||
m_loaded_blocks[p] = flags;
|
||||
}
|
||||
|
||||
if (all_new)
|
||||
m_is_dirty = false;
|
||||
}
|
||||
|
||||
std::map<v3s16, bool> MMVManip::getCoveredBlocks() const
|
||||
{
|
||||
std::map<v3s16, bool> ret;
|
||||
if (m_area.hasEmptyExtent())
|
||||
return ret;
|
||||
|
||||
// Figure out if *any* node in this block has data according to m_flags
|
||||
const auto &check_block = [this] (v3s16 bp) -> bool {
|
||||
v3s16 pmin = bp * MAP_BLOCKSIZE;
|
||||
v3s16 pmax = pmin + v3s16(MAP_BLOCKSIZE-1);
|
||||
for(s16 z=pmin.Z; z<=pmax.Z; z++)
|
||||
for(s16 y=pmin.Y; y<=pmax.Y; y++)
|
||||
for(s16 x=pmin.X; x<=pmax.X; x++) {
|
||||
if (!(m_flags[m_area.index(x,y,z)] & VOXELFLAG_NO_DATA))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
v3s16 bpmin = getNodeBlockPos(m_area.MinEdge);
|
||||
v3s16 bpmax = getNodeBlockPos(m_area.MaxEdge);
|
||||
|
||||
if (bpmin * MAP_BLOCKSIZE != m_area.MinEdge)
|
||||
throw BaseException("MMVManip not block-aligned");
|
||||
if ((bpmax+1) * MAP_BLOCKSIZE - v3s16(1) != m_area.MaxEdge)
|
||||
throw BaseException("MMVManip not block-aligned");
|
||||
|
||||
for(s16 z=bpmin.Z; z<=bpmax.Z; z++)
|
||||
for(s16 y=bpmin.Y; y<=bpmax.Y; y++)
|
||||
for(s16 x=bpmin.X; x<=bpmax.X; x++) {
|
||||
v3s16 bp(x,y,z);
|
||||
ret[bp] = check_block(bp);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
void MMVManip::blitBackAll(std::map<v3s16, MapBlock*> *modified_blocks,
|
||||
bool overwrite_generated) const
|
||||
{
|
||||
|
@ -825,16 +852,27 @@ void MMVManip::blitBackAll(std::map<v3s16, MapBlock*> *modified_blocks,
|
|||
return;
|
||||
assert(m_map);
|
||||
|
||||
/*
|
||||
Copy data of all blocks
|
||||
*/
|
||||
assert(!m_loaded_blocks.empty());
|
||||
for (auto &loaded_block : m_loaded_blocks) {
|
||||
v3s16 p = loaded_block.first;
|
||||
size_t nload = 0;
|
||||
|
||||
// Copy all the blocks with data back to the map
|
||||
const auto loaded_blocks = getCoveredBlocks();
|
||||
for (auto &it : loaded_blocks) {
|
||||
if (!it.second)
|
||||
continue;
|
||||
v3s16 p = it.first;
|
||||
MapBlock *block = m_map->getBlockNoCreateNoEx(p);
|
||||
bool existed = !(loaded_block.second & VMANIP_BLOCK_DATA_INEXIST);
|
||||
if (!existed || (block == NULL) ||
|
||||
(!overwrite_generated && block->isGenerated()))
|
||||
if (!block) {
|
||||
if (!blockpos_over_max_limit(p)) {
|
||||
block = m_map->emergeBlock(p, true);
|
||||
nload++;
|
||||
}
|
||||
}
|
||||
if (!block) {
|
||||
warningstream << "blitBackAll: Couldn't load block " << p
|
||||
<< " to write data to map" << std::endl;
|
||||
continue;
|
||||
}
|
||||
if (!overwrite_generated && block->isGenerated())
|
||||
continue;
|
||||
|
||||
block->copyFrom(*this);
|
||||
|
@ -844,6 +882,10 @@ void MMVManip::blitBackAll(std::map<v3s16, MapBlock*> *modified_blocks,
|
|||
if(modified_blocks)
|
||||
(*modified_blocks)[p] = block;
|
||||
}
|
||||
|
||||
if (nload > 0) {
|
||||
verbosestream << "blitBackAll: " << nload << " blocks had to be loaded for writing" << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
MMVManip *MMVManip::clone() const
|
||||
|
@ -860,11 +902,7 @@ MMVManip *MMVManip::clone() const
|
|||
ret->m_flags = new u8[size];
|
||||
memcpy(ret->m_flags, m_flags, size * sizeof(u8));
|
||||
}
|
||||
|
||||
ret->m_is_dirty = m_is_dirty;
|
||||
// Even if the copy is disconnected from a map object keep the information
|
||||
// needed to write it back to one
|
||||
ret->m_loaded_blocks = m_loaded_blocks;
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
|
36
src/map.h
36
src/map.h
|
@ -307,16 +307,29 @@ public:
|
|||
MMVManip(Map *map);
|
||||
virtual ~MMVManip() = default;
|
||||
|
||||
virtual void clear()
|
||||
{
|
||||
VoxelManipulator::clear();
|
||||
m_loaded_blocks.clear();
|
||||
}
|
||||
|
||||
/*
|
||||
Loads specified area from map and *adds* it to the area already
|
||||
contained in the VManip.
|
||||
*/
|
||||
void initialEmerge(v3s16 blockpos_min, v3s16 blockpos_max,
|
||||
bool load_if_inexistent = true);
|
||||
|
||||
// This is much faster with big chunks of generated data
|
||||
/**
|
||||
Uses the flags array to determine which blocks the VManip covers,
|
||||
and for which of them we have any data.
|
||||
@warning requires VManip area to be block-aligned
|
||||
@return map of blockpos -> any data?
|
||||
*/
|
||||
std::map<v3s16, bool> getCoveredBlocks() const;
|
||||
|
||||
/**
|
||||
Writes data in VManip back to the map. Blocks without any data in the VManip
|
||||
are skipped.
|
||||
@note VOXELFLAG_NO_DATA is checked per-block, not per-node. So you need
|
||||
to ensure that the relevant parts of m_data are initialized.
|
||||
@param modified_blocks output array of touched blocks (optional)
|
||||
@param overwrite_generated if false, blocks marked as generate in the map are not changed
|
||||
*/
|
||||
void blitBackAll(std::map<v3s16, MapBlock*> * modified_blocks,
|
||||
bool overwrite_generated = true) const;
|
||||
|
||||
|
@ -339,13 +352,4 @@ protected:
|
|||
|
||||
// may be null
|
||||
Map *m_map = nullptr;
|
||||
/*
|
||||
key = blockpos
|
||||
value = flags describing the block
|
||||
*/
|
||||
std::map<v3s16, u8> m_loaded_blocks;
|
||||
|
||||
enum : u8 {
|
||||
VMANIP_BLOCK_DATA_INEXIST = 1 << 0,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -40,10 +40,6 @@ void Server::handleCommand_Deprecated(NetworkPacket* pkt)
|
|||
|
||||
void Server::handleCommand_Init(NetworkPacket* pkt)
|
||||
{
|
||||
|
||||
if(pkt->getSize() < 1)
|
||||
return;
|
||||
|
||||
session_t peer_id = pkt->getPeerId();
|
||||
RemoteClient *client = getClient(peer_id, CS_Created);
|
||||
|
||||
|
@ -75,15 +71,6 @@ void Server::handleCommand_Init(NetworkPacket* pkt)
|
|||
verbosestream << "Server: Got TOSERVER_INIT from " << addr_s <<
|
||||
" (peer_id=" << peer_id << ")" << std::endl;
|
||||
|
||||
// Do not allow multiple players in simple singleplayer mode.
|
||||
// This isn't a perfect way to do it, but will suffice for now
|
||||
if (m_simple_singleplayer_mode && !m_clients.getClientIDs().empty()) {
|
||||
infostream << "Server: Not allowing another client (" << addr_s <<
|
||||
") to connect in simple singleplayer mode" << std::endl;
|
||||
DenyAccess(peer_id, SERVER_ACCESSDENIED_SINGLEPLAYER);
|
||||
return;
|
||||
}
|
||||
|
||||
if (denyIfBanned(peer_id))
|
||||
return;
|
||||
|
||||
|
@ -161,18 +148,14 @@ void Server::handleCommand_Init(NetworkPacket* pkt)
|
|||
return;
|
||||
}
|
||||
|
||||
RemotePlayer *player = m_env->getPlayer(playername, true);
|
||||
|
||||
// If player is already connected, cancel
|
||||
if (player && player->getPeerId() != PEER_ID_INEXISTENT) {
|
||||
actionstream << "Server: Player with name \"" << playername <<
|
||||
"\" tried to connect, but player with same name is already connected" << std::endl;
|
||||
DenyAccess(peer_id, SERVER_ACCESSDENIED_ALREADY_CONNECTED);
|
||||
// Do not allow multiple players in simple singleplayer mode
|
||||
if (isSingleplayer() && !m_clients.getClientIDs(CS_HelloSent).empty()) {
|
||||
infostream << "Server: Not allowing another client (" << addr_s <<
|
||||
") to connect in simple singleplayer mode" << std::endl;
|
||||
DenyAccess(peer_id, SERVER_ACCESSDENIED_SINGLEPLAYER);
|
||||
return;
|
||||
}
|
||||
|
||||
m_clients.setPlayerName(peer_id, playername);
|
||||
|
||||
// Or the "singleplayer" name to be used on regular servers
|
||||
if (!isSingleplayer() && strcasecmp(playername, "singleplayer") == 0) {
|
||||
actionstream << "Server: Player with the name \"singleplayer\" tried "
|
||||
"to connect from " << addr_s << std::endl;
|
||||
|
@ -180,12 +163,25 @@ void Server::handleCommand_Init(NetworkPacket* pkt)
|
|||
return;
|
||||
}
|
||||
|
||||
{
|
||||
RemotePlayer *player = m_env->getPlayer(playername, true);
|
||||
// If player is already connected, cancel
|
||||
if (player && player->getPeerId() != PEER_ID_INEXISTENT) {
|
||||
actionstream << "Server: Player with name \"" << playername <<
|
||||
"\" tried to connect, but player with same name is already connected" << std::endl;
|
||||
DenyAccess(peer_id, SERVER_ACCESSDENIED_ALREADY_CONNECTED);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
client->setName(playerName);
|
||||
|
||||
{
|
||||
std::string reason;
|
||||
if (m_script->on_prejoinplayer(playername, addr_s, &reason)) {
|
||||
actionstream << "Server: Player with the name \"" << playerName <<
|
||||
"\" tried to connect from " << addr_s <<
|
||||
" but it was disallowed for the following reason: " << reason <<
|
||||
" but was disallowed for the following reason: " << reason <<
|
||||
std::endl;
|
||||
DenyAccess(peer_id, SERVER_ACCESSDENIED_CUSTOM_STRING, reason);
|
||||
return;
|
||||
|
@ -195,14 +191,11 @@ void Server::handleCommand_Init(NetworkPacket* pkt)
|
|||
infostream << "Server: New connection: \"" << playerName << "\" from " <<
|
||||
addr_s << " (peer_id=" << peer_id << ")" << std::endl;
|
||||
|
||||
// Enforce user limit.
|
||||
// Don't enforce for users that have some admin right or mod permits it.
|
||||
if (m_clients.isUserLimitReached() &&
|
||||
playername != g_settings->get("name") &&
|
||||
!m_script->can_bypass_userlimit(playername, addr_s)) {
|
||||
// Early check for user limit, so the client doesn't need to run
|
||||
// through the join process only to be denied.
|
||||
if (checkUserLimit(playerName, addr_s)) {
|
||||
actionstream << "Server: " << playername << " tried to join from " <<
|
||||
addr_s << ", but there are already max_users=" <<
|
||||
g_settings->getU16("max_users") << " players." << std::endl;
|
||||
addr_s << ", but the user limit was reached." << std::endl;
|
||||
DenyAccess(peer_id, SERVER_ACCESSDENIED_TOO_MANY_USERS);
|
||||
return;
|
||||
}
|
||||
|
@ -302,6 +295,7 @@ void Server::handleCommand_Init2(NetworkPacket* pkt)
|
|||
sendMediaAnnouncement(peer_id, lang);
|
||||
|
||||
RemoteClient *client = getClient(peer_id, CS_InitDone);
|
||||
assert(client);
|
||||
|
||||
// Keep client language for server translations
|
||||
client->setLangCode(lang);
|
||||
|
@ -354,6 +348,8 @@ void Server::handleCommand_RequestMedia(NetworkPacket* pkt)
|
|||
void Server::handleCommand_ClientReady(NetworkPacket* pkt)
|
||||
{
|
||||
session_t peer_id = pkt->getPeerId();
|
||||
RemoteClient *client = getClient(peer_id, CS_Created);
|
||||
assert(client);
|
||||
|
||||
// decode all information first
|
||||
u8 major_ver, minor_ver, patch_ver, reserved;
|
||||
|
@ -364,8 +360,17 @@ void Server::handleCommand_ClientReady(NetworkPacket* pkt)
|
|||
if (pkt->getRemainingBytes() >= 2)
|
||||
*pkt >> formspec_ver;
|
||||
|
||||
m_clients.setClientVersion(peer_id, major_ver, minor_ver, patch_ver,
|
||||
full_ver);
|
||||
client->setVersionInfo(major_ver, minor_ver, patch_ver, full_ver);
|
||||
|
||||
// Since only active clients count for the user limit, two could race the
|
||||
// join process so we have to do a final check for the user limit here.
|
||||
std::string addr_s = client->getAddress().serializeString();
|
||||
if (checkUserLimit(client->getName(), addr_s)) {
|
||||
actionstream << "Server: " << client->getName() << " tried to join from " <<
|
||||
addr_s << ", but the user limit was reached (late)." << std::endl;
|
||||
DenyAccess(peer_id, SERVER_ACCESSDENIED_TOO_MANY_USERS);
|
||||
return;
|
||||
}
|
||||
|
||||
// Emerge player
|
||||
PlayerSAO* playersao = StageTwoClientInit(peer_id);
|
||||
|
@ -1426,7 +1431,7 @@ void Server::handleCommand_FirstSrp(NetworkPacket* pkt)
|
|||
|
||||
std::string salt, verification_key;
|
||||
|
||||
std::string addr_s = getPeerAddress(peer_id).serializeString();
|
||||
std::string addr_s = client->getAddress().serializeString();
|
||||
u8 is_empty;
|
||||
|
||||
*pkt >> salt >> verification_key >> is_empty;
|
||||
|
@ -1512,9 +1517,11 @@ void Server::handleCommand_SrpBytesA(NetworkPacket* pkt)
|
|||
RemoteClient *client = getClient(peer_id, CS_Invalid);
|
||||
ClientState cstate = client->getState();
|
||||
|
||||
std::string addr_s = client->getAddress().serializeString();
|
||||
|
||||
if (!((cstate == CS_HelloSent) || (cstate == CS_Active))) {
|
||||
actionstream << "Server: got SRP _A packet in wrong state " << cstate <<
|
||||
" from " << getPeerAddress(peer_id).serializeString() <<
|
||||
" from " << addr_s <<
|
||||
". Ignoring." << std::endl;
|
||||
return;
|
||||
}
|
||||
|
@ -1524,7 +1531,7 @@ void Server::handleCommand_SrpBytesA(NetworkPacket* pkt)
|
|||
if (client->chosen_mech != AUTH_MECHANISM_NONE) {
|
||||
actionstream << "Server: got SRP _A packet, while auth is already "
|
||||
"going on with mech " << client->chosen_mech << " from " <<
|
||||
getPeerAddress(peer_id).serializeString() <<
|
||||
addr_s <<
|
||||
" (wantSudo=" << wantSudo << "). Ignoring." << std::endl;
|
||||
if (wantSudo) {
|
||||
DenySudoAccess(peer_id);
|
||||
|
@ -1541,7 +1548,7 @@ void Server::handleCommand_SrpBytesA(NetworkPacket* pkt)
|
|||
|
||||
infostream << "Server: TOSERVER_SRP_BYTES_A received with "
|
||||
<< "based_on=" << int(based_on) << " and len_A="
|
||||
<< bytes_A.length() << "." << std::endl;
|
||||
<< bytes_A.length() << std::endl;
|
||||
|
||||
AuthMechanism chosen = (based_on == 0) ?
|
||||
AUTH_MECHANISM_LEGACY_PASSWORD : AUTH_MECHANISM_SRP;
|
||||
|
@ -1550,17 +1557,17 @@ void Server::handleCommand_SrpBytesA(NetworkPacket* pkt)
|
|||
// Right now, the auth mechs don't change between login and sudo mode.
|
||||
if (!client->isMechAllowed(chosen)) {
|
||||
actionstream << "Server: Player \"" << client->getName() <<
|
||||
"\" at " << getPeerAddress(peer_id).serializeString() <<
|
||||
"\" from " << addr_s <<
|
||||
" tried to change password using unallowed mech " << chosen <<
|
||||
"." << std::endl;
|
||||
std::endl;
|
||||
DenySudoAccess(peer_id);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (!client->isMechAllowed(chosen)) {
|
||||
actionstream << "Server: Client tried to authenticate from " <<
|
||||
getPeerAddress(peer_id).serializeString() <<
|
||||
" using unallowed mech " << chosen << "." << std::endl;
|
||||
addr_s <<
|
||||
" using unallowed mech " << chosen << std::endl;
|
||||
DenyAccess(peer_id, SERVER_ACCESSDENIED_UNEXPECTED_DATA);
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -261,7 +261,7 @@ int ModApiServer::l_get_player_information(lua_State *L)
|
|||
lua_settable(L, table);
|
||||
|
||||
lua_pushstring(L,"state");
|
||||
lua_pushstring(L, ClientInterface::state2Name(info.state).c_str());
|
||||
lua_pushstring(L, ClientInterface::state2Name(info.state));
|
||||
lua_settable(L, table);
|
||||
#endif
|
||||
|
||||
|
|
|
@ -44,7 +44,40 @@ int LuaVoxelManip::l_read_from_map(lua_State *L)
|
|||
|
||||
push_v3s16(L, vm->m_area.MinEdge);
|
||||
push_v3s16(L, vm->m_area.MaxEdge);
|
||||
return 2;
|
||||
}
|
||||
|
||||
int LuaVoxelManip::l_initialize(lua_State *L)
|
||||
{
|
||||
MAP_LOCK_REQUIRED;
|
||||
|
||||
LuaVoxelManip *o = checkObject<LuaVoxelManip>(L, 1);
|
||||
MMVManip *vm = o->vm;
|
||||
|
||||
if (o->is_mapgen_vm)
|
||||
throw LuaError("Cannot modify mapgen VoxelManip object");
|
||||
|
||||
VoxelArea area;
|
||||
{
|
||||
v3s16 bp1 = getNodeBlockPos(check_v3s16(L, 2));
|
||||
v3s16 bp2 = getNodeBlockPos(check_v3s16(L, 3));
|
||||
sortBoxVerticies(bp1, bp2);
|
||||
area = VoxelArea(bp1 * MAP_BLOCKSIZE, (bp2+1) * MAP_BLOCKSIZE - v3s16(1));
|
||||
}
|
||||
assert(!area.hasEmptyExtent());
|
||||
|
||||
vm->clear();
|
||||
vm->addArea(area);
|
||||
if (lua_istable(L, 4)) {
|
||||
MapNode n = readnode(L, 4);
|
||||
const u32 volume = vm->m_area.getVolume();
|
||||
for (u32 i = 0; i != volume; i++)
|
||||
vm->m_data[i] = n;
|
||||
vm->clearFlags(vm->m_area, VOXELFLAG_NO_DATA);
|
||||
}
|
||||
|
||||
push_v3s16(L, vm->m_area.MinEdge);
|
||||
push_v3s16(L, vm->m_area.MaxEdge);
|
||||
return 2;
|
||||
}
|
||||
|
||||
|
@ -93,11 +126,12 @@ int LuaVoxelManip::l_set_data(lua_State *L)
|
|||
lua_pop(L, 1);
|
||||
}
|
||||
|
||||
// FIXME: in theory we should clear VOXELFLAG_NO_DATA here
|
||||
// However there is no way to tell which values Lua code has intended to set
|
||||
// (if they were VOXELFLAG_NO_DATA before), and which were just not touched.
|
||||
// In practice this doesn't cause problems because read_from_map() will cause
|
||||
// all covered blocks to be loaded anyway.
|
||||
// Mark all data as present, since we just got it from Lua
|
||||
// Note that we can't tell if the caller intended to put CONTENT_IGNORE or
|
||||
// is just repeating the dummy values we push in l_get_data() in case
|
||||
// VOXELFLAG_NO_DATA is set. In practice this doesn't matter since ignore
|
||||
// isn't written back to the map anyway.
|
||||
vm->clearFlags(vm->m_area, VOXELFLAG_NO_DATA);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
@ -344,6 +378,19 @@ int LuaVoxelManip::l_get_emerged_area(lua_State *L)
|
|||
return 2;
|
||||
}
|
||||
|
||||
int LuaVoxelManip::l_close(lua_State *L)
|
||||
{
|
||||
NO_MAP_LOCK_REQUIRED;
|
||||
|
||||
LuaVoxelManip *o = checkObject<LuaVoxelManip>(L, 1);
|
||||
|
||||
if (o->is_mapgen_vm)
|
||||
throw LuaError("Cannot dispose of mapgen VoxelManip object");
|
||||
o->vm->clear();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
LuaVoxelManip::LuaVoxelManip(MMVManip *mmvm, bool is_mg_vm) :
|
||||
is_mapgen_vm(is_mg_vm),
|
||||
vm(mmvm)
|
||||
|
@ -436,6 +483,7 @@ void LuaVoxelManip::Register(lua_State *L)
|
|||
const char LuaVoxelManip::className[] = "VoxelManip";
|
||||
const luaL_Reg LuaVoxelManip::methods[] = {
|
||||
luamethod(LuaVoxelManip, read_from_map),
|
||||
luamethod(LuaVoxelManip, initialize),
|
||||
luamethod(LuaVoxelManip, get_data),
|
||||
luamethod(LuaVoxelManip, set_data),
|
||||
luamethod(LuaVoxelManip, get_node_at),
|
||||
|
@ -451,5 +499,6 @@ const luaL_Reg LuaVoxelManip::methods[] = {
|
|||
luamethod(LuaVoxelManip, set_param2_data),
|
||||
luamethod(LuaVoxelManip, was_modified),
|
||||
luamethod(LuaVoxelManip, get_emerged_area),
|
||||
luamethod(LuaVoxelManip, close),
|
||||
{0,0}
|
||||
};
|
||||
|
|
|
@ -24,6 +24,7 @@ private:
|
|||
static int gc_object(lua_State *L);
|
||||
|
||||
static int l_read_from_map(lua_State *L);
|
||||
static int l_initialize(lua_State *L);
|
||||
static int l_get_data(lua_State *L);
|
||||
static int l_set_data(lua_State *L);
|
||||
static int l_write_to_map(lua_State *L);
|
||||
|
@ -45,6 +46,8 @@ private:
|
|||
static int l_was_modified(lua_State *L);
|
||||
static int l_get_emerged_area(lua_State *L);
|
||||
|
||||
static int l_close(lua_State *L);
|
||||
|
||||
public:
|
||||
MMVManip *vm = nullptr;
|
||||
|
||||
|
|
|
@ -1572,7 +1572,8 @@ void Server::SendChatMessage(session_t peer_id, const ChatMessage &message)
|
|||
if (peer_id != PEER_ID_INEXISTENT) {
|
||||
Send(&pkt);
|
||||
} else {
|
||||
m_clients.sendToAll(&pkt);
|
||||
// If a client has completed auth but is still joining, still send chat
|
||||
m_clients.sendToAll(&pkt, CS_InitDone);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3183,9 +3184,7 @@ std::wstring Server::handleChat(const std::string &name,
|
|||
|
||||
ChatMessage chatmsg(line);
|
||||
|
||||
std::vector<session_t> clients = m_clients.getClientIDs();
|
||||
for (u16 cid : clients)
|
||||
SendChatMessage(cid, chatmsg);
|
||||
SendChatMessage(PEER_ID_INEXISTENT, chatmsg);
|
||||
|
||||
return L"";
|
||||
}
|
||||
|
@ -3357,6 +3356,15 @@ bool Server::denyIfBanned(session_t peer_id)
|
|||
return false;
|
||||
}
|
||||
|
||||
bool Server::checkUserLimit(const std::string &player_name, const std::string &addr_s)
|
||||
{
|
||||
if (!m_clients.isUserLimitReached())
|
||||
return false;
|
||||
if (player_name == g_settings->get("name")) // admin can always join
|
||||
return false;
|
||||
return !m_script->can_bypass_userlimit(player_name, addr_s);
|
||||
}
|
||||
|
||||
void Server::notifyPlayer(const char *name, const std::wstring &msg)
|
||||
{
|
||||
// m_env will be NULL if the server is initializing
|
||||
|
@ -3490,7 +3498,6 @@ void Server::hudSetHotbarSelectedImage(RemotePlayer *player, const std::string &
|
|||
|
||||
Address Server::getPeerAddress(session_t peer_id)
|
||||
{
|
||||
// Note that this is only set after Init was received in Server::handleCommand_Init
|
||||
return getClient(peer_id, CS_Invalid)->getAddress();
|
||||
}
|
||||
|
||||
|
|
|
@ -368,6 +368,7 @@ public:
|
|||
void hudSetHotbarImage(RemotePlayer *player, const std::string &name);
|
||||
void hudSetHotbarSelectedImage(RemotePlayer *player, const std::string &name);
|
||||
|
||||
/// @note this is only available for client state >= CS_HelloSent
|
||||
Address getPeerAddress(session_t peer_id);
|
||||
|
||||
void setLocalPlayerAnimations(RemotePlayer *player, v2f animation_frames[4],
|
||||
|
@ -611,6 +612,10 @@ private:
|
|||
|
||||
void handleChatInterfaceEvent(ChatEvent *evt);
|
||||
|
||||
/// @brief Checks if user limit allows a potential client to join
|
||||
/// @return true if the client can NOT join
|
||||
bool checkUserLimit(const std::string &player_name, const std::string &addr_s);
|
||||
|
||||
// This returns the answer to the sender of wmessage, or "" if there is none
|
||||
std::wstring handleChat(const std::string &name, std::wstring wmessage_input,
|
||||
bool check_shout_priv = false, RemotePlayer *player = nullptr);
|
||||
|
|
|
@ -47,7 +47,7 @@ const char *ClientInterface::statenames[] = {
|
|||
"SudoMode",
|
||||
};
|
||||
|
||||
std::string ClientInterface::state2Name(ClientState state)
|
||||
const char *ClientInterface::state2Name(ClientState state)
|
||||
{
|
||||
return statenames[state];
|
||||
}
|
||||
|
@ -659,7 +659,7 @@ std::vector<session_t> ClientInterface::getClientIDs(ClientState min_state)
|
|||
{
|
||||
std::vector<session_t> reply;
|
||||
RecursiveMutexAutoLock clientslock(m_clients_mutex);
|
||||
|
||||
reply.reserve(m_clients.size());
|
||||
for (const auto &m_client : m_clients) {
|
||||
if (m_client.second->getState() >= min_state)
|
||||
reply.push_back(m_client.second->peer_id);
|
||||
|
@ -677,14 +677,10 @@ void ClientInterface::markBlocksNotSent(const std::vector<v3s16> &positions, boo
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify if user limit was reached.
|
||||
* User limit count all clients from HelloSent state (MT protocol user) to Active state
|
||||
* @return true if user limit was reached
|
||||
*/
|
||||
bool ClientInterface::isUserLimitReached()
|
||||
{
|
||||
return getClientIDs(CS_HelloSent).size() >= g_settings->getU16("max_users");
|
||||
// Note that this only counts clients that have fully joined
|
||||
return getClientIDs().size() >= g_settings->getU16("max_users");
|
||||
}
|
||||
|
||||
void ClientInterface::step(float dtime)
|
||||
|
@ -703,16 +699,13 @@ void ClientInterface::step(float dtime)
|
|||
RecursiveMutexAutoLock clientslock(m_clients_mutex);
|
||||
for (const auto &it : m_clients) {
|
||||
auto state = it.second->getState();
|
||||
if (state >= CS_HelloSent)
|
||||
if (state >= CS_InitDone)
|
||||
continue;
|
||||
if (it.second->uptime() <= LINGER_TIMEOUT)
|
||||
continue;
|
||||
// CS_Created means nobody has even noticed the client is there
|
||||
// (this is before on_prejoinplayer runs)
|
||||
// CS_Invalid should not happen
|
||||
// -> log those as warning, the rest as info
|
||||
std::ostream &os = state == CS_Created || state == CS_Invalid ?
|
||||
warningstream : infostream;
|
||||
// Complain louder if this situation is unexpected
|
||||
auto &os = state == CS_Disconnecting || state == CS_Denied ?
|
||||
infostream : warningstream;
|
||||
try {
|
||||
Address addr = m_con->GetPeerAddress(it.second->peer_id);
|
||||
os << "Disconnecting lingering client from "
|
||||
|
@ -770,33 +763,22 @@ void ClientInterface::sendCustom(session_t peer_id, u8 channel, NetworkPacket *p
|
|||
m_con->Send(peer_id, channel, pkt, reliable);
|
||||
}
|
||||
|
||||
void ClientInterface::sendToAll(NetworkPacket *pkt)
|
||||
void ClientInterface::sendToAll(NetworkPacket *pkt, ClientState state_min)
|
||||
{
|
||||
auto &ccf = clientCommandFactoryTable[pkt->getCommand()];
|
||||
FATAL_ERROR_IF(!ccf.name, "packet type missing in table");
|
||||
RecursiveMutexAutoLock clientslock(m_clients_mutex);
|
||||
for (auto &client_it : m_clients) {
|
||||
RemoteClient *client = client_it.second;
|
||||
|
||||
if (client->net_proto_version != 0) {
|
||||
auto &ccf = clientCommandFactoryTable[pkt->getCommand()];
|
||||
FATAL_ERROR_IF(!ccf.name, "packet type missing in table");
|
||||
m_con->Send(client->peer_id, ccf.channel, pkt, ccf.reliable);
|
||||
}
|
||||
for (auto &[peer_id, client] : m_clients) {
|
||||
if (client->getState() >= state_min)
|
||||
m_con->Send(peer_id, ccf.channel, pkt, ccf.reliable);
|
||||
}
|
||||
}
|
||||
|
||||
RemoteClient* ClientInterface::getClientNoEx(session_t peer_id, ClientState state_min)
|
||||
{
|
||||
RecursiveMutexAutoLock clientslock(m_clients_mutex);
|
||||
RemoteClientMap::const_iterator n = m_clients.find(peer_id);
|
||||
// The client may not exist; clients are immediately removed if their
|
||||
// access is denied, and this event occurs later then.
|
||||
if (n == m_clients.end())
|
||||
return NULL;
|
||||
|
||||
if (n->second->getState() >= state_min)
|
||||
return n->second;
|
||||
|
||||
return NULL;
|
||||
RemoteClient *client = lockedGetClientNoEx(peer_id, state_min);
|
||||
return client;
|
||||
}
|
||||
|
||||
RemoteClient* ClientInterface::lockedGetClientNoEx(session_t peer_id, ClientState state_min)
|
||||
|
@ -805,12 +787,13 @@ RemoteClient* ClientInterface::lockedGetClientNoEx(session_t peer_id, ClientStat
|
|||
// The client may not exist; clients are immediately removed if their
|
||||
// access is denied, and this event occurs later then.
|
||||
if (n == m_clients.end())
|
||||
return NULL;
|
||||
return nullptr;
|
||||
|
||||
assert(n->second->peer_id == peer_id);
|
||||
if (n->second->getState() >= state_min)
|
||||
return n->second;
|
||||
|
||||
return NULL;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ClientState ClientInterface::getClientState(session_t peer_id)
|
||||
|
@ -825,16 +808,6 @@ ClientState ClientInterface::getClientState(session_t peer_id)
|
|||
return n->second->getState();
|
||||
}
|
||||
|
||||
void ClientInterface::setPlayerName(session_t peer_id, const std::string &name)
|
||||
{
|
||||
RecursiveMutexAutoLock clientslock(m_clients_mutex);
|
||||
RemoteClientMap::iterator n = m_clients.find(peer_id);
|
||||
// The client may not exist; clients are immediately removed if their
|
||||
// access is denied, and this event occurs later then.
|
||||
if (n != m_clients.end())
|
||||
n->second->setName(name);
|
||||
}
|
||||
|
||||
void ClientInterface::DeleteClient(session_t peer_id)
|
||||
{
|
||||
RecursiveMutexAutoLock conlock(m_clients_mutex);
|
||||
|
@ -915,18 +888,3 @@ u16 ClientInterface::getProtocolVersion(session_t peer_id)
|
|||
|
||||
return n->second->net_proto_version;
|
||||
}
|
||||
|
||||
void ClientInterface::setClientVersion(session_t peer_id, u8 major, u8 minor, u8 patch,
|
||||
const std::string &full)
|
||||
{
|
||||
RecursiveMutexAutoLock conlock(m_clients_mutex);
|
||||
|
||||
// Error check
|
||||
RemoteClientMap::iterator n = m_clients.find(peer_id);
|
||||
|
||||
// No client to set versions
|
||||
if (n == m_clients.end())
|
||||
return;
|
||||
|
||||
n->second->setVersionInfo(major, minor, patch, full);
|
||||
}
|
||||
|
|
|
@ -445,7 +445,7 @@ public:
|
|||
/* mark blocks as not sent on all active clients */
|
||||
void markBlocksNotSent(const std::vector<v3s16> &positions, bool low_priority = false);
|
||||
|
||||
/* verify is server user limit was reached */
|
||||
/* verify if server user limit was reached */
|
||||
bool isUserLimitReached();
|
||||
|
||||
/* get list of client player names */
|
||||
|
@ -458,7 +458,7 @@ public:
|
|||
void sendCustom(session_t peer_id, u8 channel, NetworkPacket *pkt, bool reliable);
|
||||
|
||||
/* send to all clients */
|
||||
void sendToAll(NetworkPacket *pkt);
|
||||
void sendToAll(NetworkPacket *pkt, ClientState state_min = CS_Active);
|
||||
|
||||
/* delete a client */
|
||||
void DeleteClient(session_t peer_id);
|
||||
|
@ -475,16 +475,9 @@ public:
|
|||
/* get state of client by id*/
|
||||
ClientState getClientState(session_t peer_id);
|
||||
|
||||
/* set client playername */
|
||||
void setPlayerName(session_t peer_id, const std::string &name);
|
||||
|
||||
/* get protocol version of client */
|
||||
u16 getProtocolVersion(session_t peer_id);
|
||||
|
||||
/* set client version */
|
||||
void setClientVersion(session_t peer_id, u8 major, u8 minor, u8 patch,
|
||||
const std::string &full);
|
||||
|
||||
/* event to update client state */
|
||||
void event(session_t peer_id, ClientStateEvent event);
|
||||
|
||||
|
@ -495,7 +488,8 @@ public:
|
|||
m_env = env;
|
||||
}
|
||||
|
||||
static std::string state2Name(ClientState state);
|
||||
static const char *state2Name(ClientState state);
|
||||
|
||||
protected:
|
||||
class AutoLock {
|
||||
public:
|
||||
|
@ -514,9 +508,9 @@ private:
|
|||
// Connection
|
||||
std::shared_ptr<con::IConnection> m_con;
|
||||
std::recursive_mutex m_clients_mutex;
|
||||
// Connected clients (behind the con mutex)
|
||||
// Connected clients (behind the mutex)
|
||||
RemoteClientMap m_clients;
|
||||
std::vector<std::string> m_clients_names; //for announcing masterserver
|
||||
std::vector<std::string> m_clients_names; // for announcing to server list
|
||||
|
||||
// Environment
|
||||
ServerEnvironment *m_env;
|
||||
|
@ -526,5 +520,7 @@ private:
|
|||
|
||||
static const char *statenames[];
|
||||
|
||||
static constexpr int LINGER_TIMEOUT = 10;
|
||||
// Note that this puts a fixed timeout on the init & auth phase for a client.
|
||||
// (lingering is enforced until CS_InitDone)
|
||||
static constexpr int LINGER_TIMEOUT = 12;
|
||||
};
|
||||
|
|
|
@ -264,6 +264,10 @@ void TestVoxelArea::test_intersect()
|
|||
UASSERT(v3.intersect(v1) == v1.intersect(v3));
|
||||
UASSERT(v1.intersect(v4) ==
|
||||
VoxelArea({-10, -2, -10}, {10, 2, 10}));
|
||||
|
||||
// edge cases
|
||||
UASSERT(VoxelArea().intersect(v1).hasEmptyExtent());
|
||||
UASSERT(v1.intersect(VoxelArea()).hasEmptyExtent());
|
||||
}
|
||||
|
||||
void TestVoxelArea::test_index_xyz_all_pos()
|
||||
|
|
|
@ -4,11 +4,13 @@
|
|||
|
||||
#include "test.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <memory>
|
||||
|
||||
#include "gamedef.h"
|
||||
#include "log.h"
|
||||
#include "voxel.h"
|
||||
#include "dummymap.h"
|
||||
#include "irrlicht_changes/printing.h"
|
||||
|
||||
class TestVoxelManipulator : public TestBase {
|
||||
public:
|
||||
|
@ -17,59 +19,32 @@ public:
|
|||
|
||||
void runTests(IGameDef *gamedef);
|
||||
|
||||
void testVoxelArea();
|
||||
void testVoxelManipulator(const NodeDefManager *nodedef);
|
||||
void testBasic(const NodeDefManager *nodedef);
|
||||
void testEmerge(IGameDef *gamedef);
|
||||
void testBlitBack(IGameDef *gamedef);
|
||||
void testBlitBack2(IGameDef *gamedef);
|
||||
};
|
||||
|
||||
static TestVoxelManipulator g_test_instance;
|
||||
|
||||
void TestVoxelManipulator::runTests(IGameDef *gamedef)
|
||||
{
|
||||
TEST(testVoxelArea);
|
||||
TEST(testVoxelManipulator, gamedef->getNodeDefManager());
|
||||
TEST(testBasic, gamedef->ndef());
|
||||
TEST(testEmerge, gamedef);
|
||||
TEST(testBlitBack, gamedef);
|
||||
TEST(testBlitBack2, gamedef);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
void TestVoxelManipulator::testVoxelArea()
|
||||
{
|
||||
VoxelArea a(v3s16(-1,-1,-1), v3s16(1,1,1));
|
||||
UASSERT(a.index(0,0,0) == 1*3*3 + 1*3 + 1);
|
||||
UASSERT(a.index(-1,-1,-1) == 0);
|
||||
|
||||
VoxelArea c(v3s16(-2,-2,-2), v3s16(2,2,2));
|
||||
// An area that is 1 bigger in x+ and z-
|
||||
VoxelArea d(v3s16(-2,-2,-3), v3s16(3,2,2));
|
||||
|
||||
std::list<VoxelArea> aa;
|
||||
d.diff(c, aa);
|
||||
|
||||
// Correct results
|
||||
std::vector<VoxelArea> results;
|
||||
results.emplace_back(v3s16(-2,-2,-3), v3s16(3,2,-3));
|
||||
results.emplace_back(v3s16(3,-2,-2), v3s16(3,2,2));
|
||||
|
||||
UASSERT(aa.size() == results.size());
|
||||
|
||||
infostream<<"Result of diff:"<<std::endl;
|
||||
for (auto it = aa.begin(); it != aa.end(); ++it) {
|
||||
it->print(infostream);
|
||||
infostream << std::endl;
|
||||
|
||||
auto j = std::find(results.begin(), results.end(), *it);
|
||||
UASSERT(j != results.end());
|
||||
results.erase(j);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void TestVoxelManipulator::testVoxelManipulator(const NodeDefManager *nodedef)
|
||||
void TestVoxelManipulator::testBasic(const NodeDefManager *nodedef)
|
||||
{
|
||||
VoxelManipulator v;
|
||||
|
||||
v.print(infostream, nodedef);
|
||||
UASSERT(v.m_area.hasEmptyExtent());
|
||||
|
||||
infostream << "*** Setting (-1,0,-1)=2 ***" << std::endl;
|
||||
infostream << "*** Setting (-1,0,-1) ***" << std::endl;
|
||||
v.setNode(v3s16(-1,0,-1), MapNode(t_CONTENT_GRASS));
|
||||
|
||||
v.print(infostream, nodedef);
|
||||
|
@ -89,3 +64,118 @@ void TestVoxelManipulator::testVoxelManipulator(const NodeDefManager *nodedef)
|
|||
UASSERT(v.getNode(v3s16(-1,0,-1)).getContent() == t_CONTENT_GRASS);
|
||||
EXCEPTION_CHECK(InvalidPositionException, v.getNode(v3s16(0,1,1)));
|
||||
}
|
||||
|
||||
void TestVoxelManipulator::testEmerge(IGameDef *gamedef)
|
||||
{
|
||||
constexpr int bs = MAP_BLOCKSIZE;
|
||||
|
||||
DummyMap map(gamedef, {0,0,0}, {1,1,1});
|
||||
map.fill({0,0,0}, {1,1,1}, CONTENT_AIR);
|
||||
|
||||
MMVManip vm(&map);
|
||||
UASSERT(!vm.isOrphan());
|
||||
|
||||
// emerge something
|
||||
vm.initialEmerge({0,0,0}, {0,0,0});
|
||||
UASSERTEQ(auto, vm.m_area.MinEdge, v3s16(0));
|
||||
UASSERTEQ(auto, vm.m_area.MaxEdge, v3s16(bs-1));
|
||||
UASSERTEQ(auto, vm.getNodeNoExNoEmerge({0,0,0}).getContent(), CONTENT_AIR);
|
||||
|
||||
map.setNode({0, 1,0}, t_CONTENT_BRICK);
|
||||
map.setNode({0,bs+1,0}, t_CONTENT_BRICK);
|
||||
|
||||
// emerge top block: this should not re-read the first one
|
||||
vm.initialEmerge({0,0,0}, {0,1,0});
|
||||
UASSERTEQ(auto, vm.m_area.getExtent(), v3s32(bs,2*bs,bs));
|
||||
|
||||
UASSERTEQ(auto, vm.getNodeNoExNoEmerge({0, 1,0}).getContent(), CONTENT_AIR);
|
||||
UASSERTEQ(auto, vm.getNodeNoExNoEmerge({0,bs+1,0}).getContent(), t_CONTENT_BRICK);
|
||||
|
||||
// emerge out of bounds: should produce empty data
|
||||
vm.initialEmerge({0,2,0}, {0,2,0}, false);
|
||||
UASSERTEQ(auto, vm.m_area.getExtent(), v3s32(bs,3*bs,bs));
|
||||
|
||||
UASSERTEQ(auto, vm.getNodeNoExNoEmerge({0,2*bs,0}).getContent(), CONTENT_IGNORE);
|
||||
UASSERT(!vm.exists({0,2*bs,0}));
|
||||
|
||||
// clear
|
||||
vm.clear();
|
||||
UASSERT(vm.m_area.hasEmptyExtent());
|
||||
}
|
||||
|
||||
void TestVoxelManipulator::testBlitBack(IGameDef *gamedef)
|
||||
{
|
||||
DummyMap map(gamedef, {-1,-1,-1}, {1,1,1});
|
||||
map.fill({0,0,0}, {0,0,0}, CONTENT_AIR);
|
||||
|
||||
std::unique_ptr<MMVManip> vm2;
|
||||
|
||||
{
|
||||
MMVManip vm(&map);
|
||||
vm.initialEmerge({0,0,0}, {0,0,0});
|
||||
UASSERT(vm.exists({0,0,0}));
|
||||
vm.setNodeNoEmerge({0,0,0}, t_CONTENT_STONE);
|
||||
vm.setNodeNoEmerge({1,1,1}, t_CONTENT_GRASS);
|
||||
vm.setNodeNoEmerge({2,2,2}, CONTENT_IGNORE);
|
||||
// test out clone and reparent too
|
||||
vm2.reset(vm.clone());
|
||||
}
|
||||
|
||||
UASSERT(vm2);
|
||||
UASSERT(vm2->isOrphan());
|
||||
vm2->reparent(&map);
|
||||
|
||||
std::map<v3s16, MapBlock*> modified;
|
||||
vm2->blitBackAll(&modified);
|
||||
UASSERTEQ(size_t, modified.size(), 1);
|
||||
UASSERTEQ(auto, modified.begin()->first, v3s16(0,0,0));
|
||||
|
||||
UASSERTEQ(auto, map.getNode({0,0,0}).getContent(), t_CONTENT_STONE);
|
||||
UASSERTEQ(auto, map.getNode({1,1,1}).getContent(), t_CONTENT_GRASS);
|
||||
// ignore nodes are not written (is this an intentional feature?)
|
||||
UASSERTEQ(auto, map.getNode({2,2,2}).getContent(), CONTENT_AIR);
|
||||
}
|
||||
|
||||
void TestVoxelManipulator::testBlitBack2(IGameDef *gamedef)
|
||||
{
|
||||
constexpr int bs = MAP_BLOCKSIZE;
|
||||
|
||||
DummyMap map(gamedef, {0,0,0}, {1,1,1});
|
||||
map.fill({0,0,0}, {1,1,1}, CONTENT_AIR);
|
||||
|
||||
// Create a vmanip "manually" without using initialEmerge
|
||||
MMVManip vm(&map);
|
||||
vm.addArea(VoxelArea({0,0,0}, v3s16(1,2,1) * bs - v3s16(1)));
|
||||
|
||||
// Lower block is initialized with ignore, upper with lava
|
||||
for(s16 z=0; z<bs; z++)
|
||||
for(s16 y=0; y<2*bs; y++)
|
||||
for(s16 x=0; x<bs; x++) {
|
||||
auto c = y >= bs ? t_CONTENT_LAVA : CONTENT_IGNORE;
|
||||
vm.setNodeNoEmerge({x,y,z}, c);
|
||||
}
|
||||
// But pretend the upper block was not actually initialized
|
||||
vm.setFlags(VoxelArea({0,bs,0}, v3s16(1,2,1) * bs - v3s16(1)), VOXELFLAG_NO_DATA);
|
||||
// Add a node to the lower one
|
||||
vm.setNodeNoEmerge({0,1,0}, t_CONTENT_TORCH);
|
||||
|
||||
// Verify covered blocks
|
||||
{
|
||||
auto cov = vm.getCoveredBlocks();
|
||||
UASSERTEQ(size_t, cov.size(), 2);
|
||||
auto it = cov.find({0,0,0});
|
||||
UASSERT(it != cov.end() && it->second);
|
||||
it = cov.find({0,1,0});
|
||||
UASSERT(it != cov.end() && !it->second);
|
||||
}
|
||||
|
||||
// Now blit it back
|
||||
std::map<v3s16, MapBlock*> modified;
|
||||
vm.blitBackAll(&modified);
|
||||
// The lower block data should have been written
|
||||
UASSERTEQ(size_t, modified.size(), 1);
|
||||
UASSERTEQ(auto, modified.begin()->first, v3s16(0,0,0));
|
||||
UASSERTEQ(auto, map.getNode({0,1,0}).getContent(), t_CONTENT_TORCH);
|
||||
// The upper one should not!
|
||||
UASSERTEQ(auto, map.getNode({0,bs,0}).getContent(), CONTENT_AIR);
|
||||
}
|
||||
|
|
|
@ -15,7 +15,6 @@
|
|||
Debug stuff
|
||||
*/
|
||||
u64 emerge_time = 0;
|
||||
u64 emerge_load_time = 0;
|
||||
|
||||
VoxelManipulator::~VoxelManipulator()
|
||||
{
|
||||
|
|
|
@ -34,7 +34,6 @@ class NodeDefManager;
|
|||
Debug stuff
|
||||
*/
|
||||
extern u64 emerge_time;
|
||||
extern u64 emerge_load_time;
|
||||
|
||||
/*
|
||||
This class resembles aabbox3d<s16> a lot, but has inclusive
|
||||
|
@ -469,7 +468,7 @@ public:
|
|||
Control
|
||||
*/
|
||||
|
||||
virtual void clear();
|
||||
void clear();
|
||||
|
||||
void print(std::ostream &o, const NodeDefManager *nodemgr,
|
||||
VoxelPrintMode mode=VOXELPRINT_MATERIAL) const;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue