diff --git a/builtin/common/settings/generate_from_settingtypes.lua b/builtin/common/settings/generate_from_settingtypes.lua index 4c33a8fc1a..13b3035cef 100644 --- a/builtin/common/settings/generate_from_settingtypes.lua +++ b/builtin/common/settings/generate_from_settingtypes.lua @@ -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 diff --git a/doc/lua_api.md b/doc/lua_api.md index 747c523a4d..3a6da1bdb8 100644 --- a/doc/lua_api.md +++ b/doc/lua_api.md @@ -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` ----------- diff --git a/doc/menu_lua_api.md b/doc/menu_lua_api.md index 0a067764e9..56b51f3b9f 100644 --- a/doc/menu_lua_api.md +++ b/doc/menu_lua_api.md @@ -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 diff --git a/src/client/game_formspec.cpp b/src/client/game_formspec.cpp index dc51247b0e..3d8dc6fc8d 100644 --- a/src/client/game_formspec.cpp +++ b/src/client/game_formspec.cpp @@ -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); diff --git a/src/dummymap.h b/src/dummymap.h index 2da3718848..408f91341d 100644 --- a/src/dummymap.h +++ b/src/dummymap.h @@ -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(); } } diff --git a/src/gui/guiEngine.cpp b/src/gui/guiEngine.cpp index b31f12ef9a..0ec0d7a764 100644 --- a/src/gui/guiEngine.cpp +++ b/src/gui/guiEngine.cpp @@ -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() { diff --git a/src/gui/guiEngine.h b/src/gui/guiEngine.h index 5c95e9a4c2..f4b22f3c26 100644 --- a/src/gui/guiEngine.h +++ b/src/gui/guiEngine.h @@ -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; diff --git a/src/gui/guiFormSpecMenu.cpp b/src/gui/guiFormSpecMenu.cpp index f1e91bd0b8..97d86b05f8 100644 --- a/src/gui/guiFormSpecMenu.cpp +++ b/src/gui/guiFormSpecMenu.cpp @@ -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." <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 &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 &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; } } diff --git a/src/gui/guiFormSpecMenu.h b/src/gui/guiFormSpecMenu.h index 8dbfcab93f..f279bbb298 100644 --- a/src/gui/guiFormSpecMenu.h +++ b/src/gui/guiFormSpecMenu.h @@ -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 *parsed_rect); void tryClose(); + void trySubmitClose(); void showTooltip(const std::wstring &text, const irr::video::SColor &color, const irr::video::SColor &bgcolor); diff --git a/src/map.cpp b/src/map.cpp index d882008465..964d8b5afa 100644 --- a/src/map.cpp +++ b/src/map.cpp @@ -772,52 +772,79 @@ void MMVManip::initialEmerge(v3s16 p_min, v3s16 p_max, bool load_if_inexistent) infostream< 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 MMVManip::getCoveredBlocks() const +{ + std::map 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 *modified_blocks, bool overwrite_generated) const { @@ -825,16 +852,27 @@ void MMVManip::blitBackAll(std::map *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 *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; } diff --git a/src/map.h b/src/map.h index 9ec66cfb93..57ede8ee68 100644 --- a/src/map.h +++ b/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 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 * 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 m_loaded_blocks; - - enum : u8 { - VMANIP_BLOCK_DATA_INEXIST = 1 << 0, - }; }; diff --git a/src/network/serverpackethandler.cpp b/src/network/serverpackethandler.cpp index d9a38e8787..e9012d3519 100644 --- a/src/network/serverpackethandler.cpp +++ b/src/network/serverpackethandler.cpp @@ -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; } diff --git a/src/script/lua_api/l_server.cpp b/src/script/lua_api/l_server.cpp index 74ee5e5c9a..698b2dba67 100644 --- a/src/script/lua_api/l_server.cpp +++ b/src/script/lua_api/l_server.cpp @@ -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 diff --git a/src/script/lua_api/l_vmanip.cpp b/src/script/lua_api/l_vmanip.cpp index 5815d3bf3d..af254b1056 100644 --- a/src/script/lua_api/l_vmanip.cpp +++ b/src/script/lua_api/l_vmanip.cpp @@ -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(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(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} }; diff --git a/src/script/lua_api/l_vmanip.h b/src/script/lua_api/l_vmanip.h index 5ba1caffa4..95bb82ce22 100644 --- a/src/script/lua_api/l_vmanip.h +++ b/src/script/lua_api/l_vmanip.h @@ -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; diff --git a/src/server.cpp b/src/server.cpp index 008b91022b..a1263ef1dc 100644 --- a/src/server.cpp +++ b/src/server.cpp @@ -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 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(); } diff --git a/src/server.h b/src/server.h index c9869e1ddf..ae5b106828 100644 --- a/src/server.h +++ b/src/server.h @@ -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); diff --git a/src/server/clientiface.cpp b/src/server/clientiface.cpp index 13cf14c07a..bbcd4720db 100644 --- a/src/server/clientiface.cpp +++ b/src/server/clientiface.cpp @@ -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 ClientInterface::getClientIDs(ClientState min_state) { std::vector 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 &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); -} diff --git a/src/server/clientiface.h b/src/server/clientiface.h index 44b278e63e..d0e91dcca3 100644 --- a/src/server/clientiface.h +++ b/src/server/clientiface.h @@ -445,7 +445,7 @@ public: /* mark blocks as not sent on all active clients */ void markBlocksNotSent(const std::vector &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 m_con; std::recursive_mutex m_clients_mutex; - // Connected clients (behind the con mutex) + // Connected clients (behind the mutex) RemoteClientMap m_clients; - std::vector m_clients_names; //for announcing masterserver + std::vector 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; }; diff --git a/src/unittest/test_voxelarea.cpp b/src/unittest/test_voxelarea.cpp index d9380caf9e..52bf1bfd89 100644 --- a/src/unittest/test_voxelarea.cpp +++ b/src/unittest/test_voxelarea.cpp @@ -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() diff --git a/src/unittest/test_voxelmanipulator.cpp b/src/unittest/test_voxelmanipulator.cpp index 09d4b5781e..9c1f85d242 100644 --- a/src/unittest/test_voxelmanipulator.cpp +++ b/src/unittest/test_voxelmanipulator.cpp @@ -4,11 +4,13 @@ #include "test.h" -#include +#include #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 aa; - d.diff(c, aa); - - // Correct results - std::vector 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:"<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 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 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 ? 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 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); +} diff --git a/src/voxel.cpp b/src/voxel.cpp index 4d0ce84b7f..5a69b07255 100644 --- a/src/voxel.cpp +++ b/src/voxel.cpp @@ -15,7 +15,6 @@ Debug stuff */ u64 emerge_time = 0; -u64 emerge_load_time = 0; VoxelManipulator::~VoxelManipulator() { diff --git a/src/voxel.h b/src/voxel.h index c47f97a300..7fbaadc281 100644 --- a/src/voxel.h +++ b/src/voxel.h @@ -34,7 +34,6 @@ class NodeDefManager; Debug stuff */ extern u64 emerge_time; -extern u64 emerge_load_time; /* This class resembles aabbox3d 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;