diff --git a/src/server/CMakeLists.txt b/src/server/CMakeLists.txt index 3588451c0..98318e93d 100644 --- a/src/server/CMakeLists.txt +++ b/src/server/CMakeLists.txt @@ -1,13 +1,14 @@ set(common_server_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/activeobjectmgr.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ban.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/blockmodifier.cpp ${CMAKE_CURRENT_SOURCE_DIR}/clientiface.cpp ${CMAKE_CURRENT_SOURCE_DIR}/luaentity_sao.cpp ${CMAKE_CURRENT_SOURCE_DIR}/mods.cpp ${CMAKE_CURRENT_SOURCE_DIR}/player_sao.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/rollback.cpp ${CMAKE_CURRENT_SOURCE_DIR}/serveractiveobject.cpp ${CMAKE_CURRENT_SOURCE_DIR}/serverinventorymgr.cpp ${CMAKE_CURRENT_SOURCE_DIR}/serverlist.cpp ${CMAKE_CURRENT_SOURCE_DIR}/unit_sao.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/rollback.cpp PARENT_SCOPE) diff --git a/src/server/blockmodifier.cpp b/src/server/blockmodifier.cpp new file mode 100644 index 000000000..1983d5def --- /dev/null +++ b/src/server/blockmodifier.cpp @@ -0,0 +1,542 @@ +// Luanti +// SPDX-License-Identifier: LGPL-2.1-or-later +// Copyright (C) 2010-2017 celeron55, Perttu Ahola + +#include +#include "blockmodifier.h" +#include "serverenvironment.h" +#include "server.h" +#include "mapblock.h" +#include "nodedef.h" +#include "gamedef.h" + +/* + ABMs +*/ + +ABMWithState::ABMWithState(ActiveBlockModifier *abm_): + abm(abm_) +{ + // Initialize timer to random value to spread processing + float itv = abm->getTriggerInterval(); + itv = MYMAX(0.001f, itv); // No less than 1ms + int minval = MYMAX(-0.51f * itv, -60); // Clamp to + int maxval = MYMIN( 0.51f * itv, 60); // +-60 seconds + timer = myrand_range(minval, maxval); +} + +struct ActiveABM +{ + ActiveBlockModifier *abm; + std::vector required_neighbors; + std::vector without_neighbors; + int chance; + s16 min_y, max_y; +}; + +#define CONTENT_TYPE_CACHE_MAX 64 + +ABMHandler::ABMHandler(std::vector &abms, + float dtime_s, ServerEnvironment *env, + bool use_timers): + m_env(env) +{ + if (dtime_s < 0.001f) + return; + const NodeDefManager *ndef = env->getGameDef()->ndef(); + for (ABMWithState &abmws : abms) { + ActiveBlockModifier *abm = abmws.abm; + float trigger_interval = abm->getTriggerInterval(); + if (trigger_interval < 0.001f) + trigger_interval = 0.001f; + float actual_interval = dtime_s; + if (use_timers) { + abmws.timer += dtime_s; + if (abmws.timer < trigger_interval) + continue; + abmws.timer -= trigger_interval; + actual_interval = trigger_interval; + } + float chance = abm->getTriggerChance(); + if (chance == 0) + chance = 1; + + ActiveABM aabm; + aabm.abm = abm; + if (abm->getSimpleCatchUp()) { + float intervals = actual_interval / trigger_interval; + if (intervals == 0) + continue; + aabm.chance = chance / intervals; + if (aabm.chance == 0) + aabm.chance = 1; + } else { + aabm.chance = chance; + } + // y limits + aabm.min_y = abm->getMinY(); + aabm.max_y = abm->getMaxY(); + + // Trigger neighbors + for (const auto &s : abm->getRequiredNeighbors()) + ndef->getIds(s, aabm.required_neighbors); + SORT_AND_UNIQUE(aabm.required_neighbors); + + for (const auto &s : abm->getWithoutNeighbors()) + ndef->getIds(s, aabm.without_neighbors); + SORT_AND_UNIQUE(aabm.without_neighbors); + + // Trigger contents + std::vector ids; + for (const auto &s : abm->getTriggerContents()) + ndef->getIds(s, ids); + SORT_AND_UNIQUE(ids); + for (content_t c : ids) { + if (c >= m_aabms.size()) + m_aabms.resize(c + 256, nullptr); + if (!m_aabms[c]) + m_aabms[c] = new std::vector; + m_aabms[c]->push_back(aabm); + } + } +} + +ABMHandler::~ABMHandler() +{ + for (auto &aabms : m_aabms) + delete aabms; +} + +u32 ABMHandler::countObjects(MapBlock *block, ServerMap *map, u32 &wider) +{ + wider = 0; + u32 wider_unknown_count = 0; + for(s16 x=-1; x<=1; x++) + for(s16 y=-1; y<=1; y++) + for(s16 z=-1; z<=1; z++) + { + MapBlock *block2 = map->getBlockNoCreateNoEx( + block->getPos() + v3s16(x,y,z)); + if (!block2) { + wider_unknown_count++; + continue; + } + wider += block2->m_static_objects.size(); + } + // Extrapolate + u32 active_object_count = block->m_static_objects.getActiveSize(); + u32 wider_known_count = 3 * 3 * 3 - wider_unknown_count; + wider += wider_unknown_count * wider / wider_known_count; + return active_object_count; +} + +void ABMHandler::apply(MapBlock *block, int &blocks_scanned, int &abms_run, int &blocks_cached) +{ + if (m_aabms.empty()) + return; + + // Check the content type cache first + // to see whether there are any ABMs + // to be run at all for this block. + if (!block->contents.empty()) { + assert(!block->do_not_cache_contents); // invariant + blocks_cached++; + bool run_abms = false; + for (content_t c : block->contents) { + if (c < m_aabms.size() && m_aabms[c]) { + run_abms = true; + break; + } + } + if (!run_abms) + return; + } + blocks_scanned++; + + ServerMap *map = &m_env->getServerMap(); + + u32 active_object_count_wider; + u32 active_object_count = countObjects(block, map, active_object_count_wider); + m_env->m_added_objects = 0; + + bool want_contents_cached = block->contents.empty() && !block->do_not_cache_contents; + + v3s16 p0; + for(p0.Z=0; p0.ZgetNodeNoCheck(p0); + content_t c = n.getContent(); + + // Cache content types as we go + if (want_contents_cached && !CONTAINS(block->contents, c)) { + if (block->contents.size() >= CONTENT_TYPE_CACHE_MAX) { + // Too many different nodes... don't try to cache + want_contents_cached = false; + block->do_not_cache_contents = true; + decltype(block->contents) empty; + std::swap(block->contents, empty); + } else { + block->contents.push_back(c); + } + } + + if (c >= m_aabms.size() || !m_aabms[c]) + continue; + + v3s16 p = p0 + block->getPosRelative(); + for (ActiveABM &aabm : *m_aabms[c]) { + if (p.Y < aabm.min_y || p.Y > aabm.max_y) + continue; + + if (myrand() % aabm.chance != 0) + continue; + + // Check neighbors + const bool check_required_neighbors = !aabm.required_neighbors.empty(); + const bool check_without_neighbors = !aabm.without_neighbors.empty(); + if (check_required_neighbors || check_without_neighbors) { + v3s16 p1; + bool have_required = false; + for(p1.X = p0.X-1; p1.X <= p0.X+1; p1.X++) + for(p1.Y = p0.Y-1; p1.Y <= p0.Y+1; p1.Y++) + for(p1.Z = p0.Z-1; p1.Z <= p0.Z+1; p1.Z++) + { + if (p1 == p0) + continue; + content_t c; + if (block->isValidPosition(p1)) { + // if the neighbor is found on the same map block + // get it straight from there + const MapNode &n = block->getNodeNoCheck(p1); + c = n.getContent(); + } else { + // otherwise consult the map + MapNode n = map->getNode(p1 + block->getPosRelative()); + c = n.getContent(); + } + if (check_required_neighbors && !have_required) { + if (CONTAINS(aabm.required_neighbors, c)) { + if (!check_without_neighbors) + goto neighbor_found; + have_required = true; + } + } + if (check_without_neighbors) { + if (CONTAINS(aabm.without_neighbors, c)) + goto neighbor_invalid; + } + } + if (have_required || !check_required_neighbors) + goto neighbor_found; + // No required neighbor found +neighbor_invalid: + continue; + } + +neighbor_found: + + abms_run++; + // Call all the trigger variations + aabm.abm->trigger(m_env, p, n); + aabm.abm->trigger(m_env, p, n, + active_object_count, active_object_count_wider); + + if (block->isOrphan()) + return; + + // Count surrounding objects again if the abms added any + if (m_env->m_added_objects > 0) { + active_object_count = countObjects(block, map, active_object_count_wider); + m_env->m_added_objects = 0; + } + + // Update and check node after possible modification + n = block->getNodeNoCheck(p0); + if (n.getContent() != c) + break; + } + } +} + +/* + LBMs +*/ + +LBMContentMapping::~LBMContentMapping() +{ + map.clear(); + for (auto &it : lbm_list) + delete it; +} + +void LBMContentMapping::addLBM(LoadingBlockModifierDef *lbm_def, IGameDef *gamedef) +{ + // Add the lbm_def to the LBMContentMapping. + // Unknown names get added to the global NameIdMapping. + const NodeDefManager *nodedef = gamedef->ndef(); + + FATAL_ERROR_IF(CONTAINS(lbm_list, lbm_def), "Same LBM registered twice"); + lbm_list.push_back(lbm_def); + + std::vector c_ids; + + for (const auto &node : lbm_def->trigger_contents) { + bool found = nodedef->getIds(node, c_ids); + if (!found) { + content_t c_id = gamedef->allocateUnknownNodeId(node); + if (c_id == CONTENT_IGNORE) { + // Seems it can't be allocated. + warningstream << "Could not internalize node name \"" << node + << "\" while loading LBM \"" << lbm_def->name << "\"." << std::endl; + continue; + } + c_ids.push_back(c_id); + } + } + + SORT_AND_UNIQUE(c_ids); + + for (content_t c_id : c_ids) + map[c_id].push_back(lbm_def); +} + +const LBMContentMapping::lbm_vector * +LBMContentMapping::lookup(content_t c) const +{ + lbm_map::const_iterator it = map.find(c); + if (it == map.end()) + return nullptr; + return &(it->second); +} + +LBMManager::~LBMManager() +{ + for (auto &m_lbm_def : m_lbm_defs) + delete m_lbm_def.second; + + m_lbm_lookup.clear(); +} + +void LBMManager::addLBMDef(LoadingBlockModifierDef *lbm_def) +{ + // Precondition, in query mode the map isn't used anymore + FATAL_ERROR_IF(m_query_mode, + "attempted to modify LBMManager in query mode"); + + if (str_starts_with(lbm_def->name, ":")) + lbm_def->name.erase(0, 1); + + if (lbm_def->name.empty() || + !string_allowed(lbm_def->name, LBM_NAME_ALLOWED_CHARS)) { + throw ModError("Error adding LBM \"" + lbm_def->name + + "\": Does not follow naming conventions: " + "Only characters [a-z0-9_:] are allowed."); + } + + m_lbm_defs[lbm_def->name] = lbm_def; +} + +void LBMManager::loadIntroductionTimes(const std::string ×, + IGameDef *gamedef, u32 now) +{ + m_query_mode = true; + + auto introduction_times = parseIntroductionTimesString(times); + + // Put stuff from introduction_times into m_lbm_lookup + for (auto &[name, time] : introduction_times) { + auto def_it = m_lbm_defs.find(name); + if (def_it == m_lbm_defs.end()) { + infostream << "LBMManager: LBM " << name << " is not registered. " + "Discarding it." << std::endl; + continue; + } + auto *lbm_def = def_it->second; + if (lbm_def->run_at_every_load) { + continue; // These are handled below + } + if (time > now) { + warningstream << "LBMManager: LBM " << name << " was introduced in " + "the future. Pretending it's new." << std::endl; + // By skipping here it will be added as newly introduced. + continue; + } + + m_lbm_lookup[time].addLBM(lbm_def, gamedef); + + // Erase the entry so that we know later + // which elements didn't get put into m_lbm_lookup + m_lbm_defs.erase(def_it); + } + + // Now also add the elements from m_lbm_defs to m_lbm_lookup + // that weren't added in the previous step. + // They are introduced first time to this world, + // or are run at every load (introduction time hardcoded to U32_MAX). + + auto &lbms_we_introduce_now = m_lbm_lookup[now]; + auto &lbms_running_always = m_lbm_lookup[U32_MAX]; + for (auto &it : m_lbm_defs) { + if (it.second->run_at_every_load) + lbms_running_always.addLBM(it.second, gamedef); + else + lbms_we_introduce_now.addLBM(it.second, gamedef); + } + + // All pointer ownership now moved to LBMContentMapping + m_lbm_defs.clear(); + + // If these are empty delete them again to avoid pointless iteration. + if (lbms_we_introduce_now.empty()) + m_lbm_lookup.erase(now); + if (lbms_running_always.empty()) + m_lbm_lookup.erase(U32_MAX); + + infostream << "LBMManager: " << m_lbm_lookup.size() << + " unique times in lookup table" << std::endl; +} + +std::string LBMManager::createIntroductionTimesString() +{ + // Precondition, we must be in query mode + FATAL_ERROR_IF(!m_query_mode, + "attempted to query on non fully set up LBMManager"); + + std::ostringstream oss; + for (const auto &it : m_lbm_lookup) { + u32 time = it.first; + auto &lbm_list = it.second.getList(); + for (const auto &lbm_def : lbm_list) { + // Don't add if the LBM runs at every load, + // then introduction time is hardcoded and doesn't need to be stored. + if (lbm_def->run_at_every_load) + continue; + oss << lbm_def->name << "~" << time << ";"; + } + } + return oss.str(); +} + +std::unordered_map + LBMManager::parseIntroductionTimesString(const std::string ×) +{ + std::unordered_map ret; + + size_t idx = 0; + size_t idx_new; + while ((idx_new = times.find(';', idx)) != std::string::npos) { + std::string entry = times.substr(idx, idx_new - idx); + idx = idx_new + 1; + + std::vector components = str_split(entry, '~'); + if (components.size() != 2) + throw SerializationError("Introduction times entry \"" + + entry + "\" requires exactly one '~'!"); + if (components[0].empty()) + throw SerializationError("LBM name is empty"); + std::string name = std::move(components[0]); + if (name.front() == ':') // old versions didn't strip this + name.erase(0, 1); + u32 time = from_string(components[1]); + ret[std::move(name)] = time; + } + + return ret; +} + +namespace { + struct LBMToRun { + std::unordered_set p; // node positions + std::vector l; // ordered list of LBMs + + template + void insertLBMs(const C &container) { + for (auto &it : container) { + if (!CONTAINS(l, it)) + l.push_back(it); + } + } + }; +} + +void LBMManager::applyLBMs(ServerEnvironment *env, MapBlock *block, + const u32 stamp, const float dtime_s) +{ + // Precondition, we need m_lbm_lookup to be initialized + FATAL_ERROR_IF(!m_query_mode, + "attempted to query on non fully set up LBMManager"); + + // Collect a list of all LBMs and associated positions + std::unordered_map to_run; + + // Note: the iteration count of this outer loop is typically very low, so it's ok. + for (auto it = getLBMsIntroducedAfter(stamp); it != m_lbm_lookup.end(); ++it) { + v3s16 pos; + content_t c; + + // Cache previous lookups since it has a high performance penalty. + content_t previous_c = CONTENT_IGNORE; + const LBMContentMapping::lbm_vector *lbm_list = nullptr; + LBMToRun *batch = nullptr; + + for (pos.Z = 0; pos.Z < MAP_BLOCKSIZE; pos.Z++) + for (pos.Y = 0; pos.Y < MAP_BLOCKSIZE; pos.Y++) + for (pos.X = 0; pos.X < MAP_BLOCKSIZE; pos.X++) { + c = block->getNodeNoCheck(pos).getContent(); + + bool c_changed = false; + if (previous_c != c) { + c_changed = true; + lbm_list = it->second.lookup(c); + if (lbm_list) + batch = &to_run[c]; // creates entry + previous_c = c; + } + + if (!lbm_list) + continue; + batch->p.insert(pos); + if (c_changed) { + batch->insertLBMs(*lbm_list); + } else { + // we were here before so the list must be filled + assert(!batch->l.empty()); + } + } + } + + // Actually run them + bool first = true; + for (auto &[c, batch] : to_run) { + if (tracestream) { + tracestream << "Running " << batch.l.size() << " LBMs for node " + << env->getGameDef()->ndef()->get(c).name << " (" + << batch.p.size() << "x) in block " << block->getPos() << std::endl; + } + for (auto &lbm_def : batch.l) { + if (!first) { + // The fun part: since any LBM call can change the nodes inside of he + // block, we have to recheck the positions to see if the wanted node + // is still there. + // Note that we don't rescan the whole block, we don't want to include new changes. + for (auto it2 = batch.p.begin(); it2 != batch.p.end(); ) { + if (block->getNodeNoCheck(*it2).getContent() != c) + it2 = batch.p.erase(it2); + else + ++it2; + } + } else { + assert(!batch.p.empty()); + } + first = false; + + if (batch.p.empty()) + break; + lbm_def->trigger(env, block, batch.p, dtime_s); + if (block->isOrphan()) + return; + } + } +} diff --git a/src/server/blockmodifier.h b/src/server/blockmodifier.h new file mode 100644 index 000000000..602147ae0 --- /dev/null +++ b/src/server/blockmodifier.h @@ -0,0 +1,176 @@ +// Luanti +// SPDX-License-Identifier: LGPL-2.1-or-later +// Copyright (C) 2010-2017 celeron55, Perttu Ahola + +#pragma once + +#include +#include +#include +#include + +#include "irr_v3d.h" +#include "mapnode.h" + +class ServerEnvironment; +class ServerMap; +class MapBlock; +class IGameDef; + +/* + ABMs +*/ + +class ActiveBlockModifier +{ +public: + ActiveBlockModifier() = default; + virtual ~ActiveBlockModifier() = default; + + // Set of contents to trigger on + virtual const std::vector &getTriggerContents() const = 0; + // Set of required neighbors (trigger doesn't happen if none are found) + // Empty = do not check neighbors + virtual const std::vector &getRequiredNeighbors() const = 0; + // Set of without neighbors (trigger doesn't happen if any are found) + // Empty = do not check neighbors + virtual const std::vector &getWithoutNeighbors() const = 0; + // Trigger interval in seconds + virtual float getTriggerInterval() = 0; + // Random chance of (1 / return value), 0 is disallowed + virtual u32 getTriggerChance() = 0; + // Whether to modify chance to simulate time lost by an unnattended block + virtual bool getSimpleCatchUp() = 0; + // get min Y for apply abm + virtual s16 getMinY() = 0; + // get max Y for apply abm + virtual s16 getMaxY() = 0; + // This is called usually at interval for 1/chance of the nodes + virtual void trigger(ServerEnvironment *env, v3s16 p, MapNode n){}; + virtual void trigger(ServerEnvironment *env, v3s16 p, MapNode n, + u32 active_object_count, u32 active_object_count_wider){}; +}; + +struct ABMWithState +{ + ActiveBlockModifier *abm; + float timer = 0.0f; + + ABMWithState(ActiveBlockModifier *abm_); +}; + +struct ActiveABM; // hidden + +class ABMHandler +{ + ServerEnvironment *m_env; + // vector index = content_t + std::vector*> m_aabms; + +public: + ABMHandler(std::vector &abms, + float dtime_s, ServerEnvironment *env, + bool use_timers); + ~ABMHandler(); + + // Find out how many objects the given block and its neighbors contain. + // Returns the number of objects in the block, and also in 'wider' the + // number of objects in the block and all its neighbors. The latter + // may be an estimate if any neighbors are unloaded. + static u32 countObjects(MapBlock *block, ServerMap * map, u32 &wider); + + void apply(MapBlock *block, int &blocks_scanned, int &abms_run, int &blocks_cached); +}; + +/* + LBMs +*/ + +#define LBM_NAME_ALLOWED_CHARS "abcdefghijklmnopqrstuvwxyz0123456789_:" + +struct LoadingBlockModifierDef +{ + // Set of contents to trigger on + std::vector trigger_contents; + std::string name; + bool run_at_every_load = false; + + virtual ~LoadingBlockModifierDef() = default; + + /// @brief Called to invoke LBM + /// @param env environment + /// @param block the block in question + /// @param positions set of node positions (block-relative!) + /// @param dtime_s game time since last deactivation + virtual void trigger(ServerEnvironment *env, MapBlock *block, + const std::unordered_set &positions, float dtime_s) {}; +}; + +class LBMContentMapping +{ +public: + typedef std::vector lbm_vector; + typedef std::unordered_map lbm_map; + + LBMContentMapping() = default; + void addLBM(LoadingBlockModifierDef *lbm_def, IGameDef *gamedef); + const lbm_map::mapped_type *lookup(content_t c) const; + const lbm_vector &getList() const { return lbm_list; } + bool empty() const { return lbm_list.empty(); } + + // This struct owns the LBM pointers. + ~LBMContentMapping(); + DISABLE_CLASS_COPY(LBMContentMapping); + ALLOW_CLASS_MOVE(LBMContentMapping); + +private: + lbm_vector lbm_list; + lbm_map map; +}; + +class LBMManager +{ +public: + LBMManager() = default; + ~LBMManager(); + + // Don't call this after loadIntroductionTimes() ran. + void addLBMDef(LoadingBlockModifierDef *lbm_def); + + /// @param now current game time + void loadIntroductionTimes(const std::string ×, + IGameDef *gamedef, u32 now); + + // Don't call this before loadIntroductionTimes() ran. + std::string createIntroductionTimesString(); + + // Don't call this before loadIntroductionTimes() ran. + void applyLBMs(ServerEnvironment *env, MapBlock *block, + u32 stamp, float dtime_s); + + // Warning: do not make this std::unordered_map, order is relevant here + typedef std::map lbm_lookup_map; + +private: + // Once we set this to true, we can only query, + // not modify + bool m_query_mode = false; + + // For m_query_mode == false: + // The key of the map is the LBM def's name. + std::unordered_map m_lbm_defs; + + // For m_query_mode == true: + // The key of the map is the LBM def's first introduction time. + lbm_lookup_map m_lbm_lookup; + + /// @return map of LBM name -> timestamp + static std::unordered_map + parseIntroductionTimesString(const std::string ×); + + // Returns an iterator to the LBMs that were introduced + // after the given time. This is guaranteed to return + // valid values for everything + lbm_lookup_map::const_iterator getLBMsIntroducedAfter(u32 time) + { return m_lbm_lookup.lower_bound(time); } +}; diff --git a/src/serverenvironment.cpp b/src/serverenvironment.cpp index 90127ab6f..697b7b073 100644 --- a/src/serverenvironment.cpp +++ b/src/serverenvironment.cpp @@ -38,8 +38,6 @@ #include "server/luaentity_sao.h" #include "server/player_sao.h" -#define LBM_NAME_ALLOWED_CHARS "abcdefghijklmnopqrstuvwxyz0123456789_:" - // A number that is much smaller than the timeout for particle spawners should/could ever be #define PARTICLE_SPAWNER_NO_EXPIRY -1024.f @@ -47,305 +45,6 @@ static constexpr s16 ACTIVE_OBJECT_RESAVE_DISTANCE_SQ = sqr(3); static constexpr u32 BLOCK_RESAVE_TIMESTAMP_DIFF = 60; // in units of game time -/* - ABMWithState -*/ - -ABMWithState::ABMWithState(ActiveBlockModifier *abm_): - abm(abm_) -{ - // Initialize timer to random value to spread processing - float itv = abm->getTriggerInterval(); - itv = MYMAX(0.001f, itv); // No less than 1ms - int minval = MYMAX(-0.51f*itv, -60); // Clamp to - int maxval = MYMIN(0.51f*itv, 60); // +-60 seconds - timer = myrand_range(minval, maxval); -} - -/* - LBMManager -*/ - -LBMContentMapping::~LBMContentMapping() -{ - map.clear(); - for (auto &it : lbm_list) - delete it; -} - -void LBMContentMapping::addLBM(LoadingBlockModifierDef *lbm_def, IGameDef *gamedef) -{ - // Add the lbm_def to the LBMContentMapping. - // Unknown names get added to the global NameIdMapping. - const NodeDefManager *nodedef = gamedef->ndef(); - - FATAL_ERROR_IF(CONTAINS(lbm_list, lbm_def), "Same LBM registered twice"); - lbm_list.push_back(lbm_def); - - std::vector c_ids; - - for (const auto &node : lbm_def->trigger_contents) { - bool found = nodedef->getIds(node, c_ids); - if (!found) { - content_t c_id = gamedef->allocateUnknownNodeId(node); - if (c_id == CONTENT_IGNORE) { - // Seems it can't be allocated. - warningstream << "Could not internalize node name \"" << node - << "\" while loading LBM \"" << lbm_def->name << "\"." << std::endl; - continue; - } - c_ids.push_back(c_id); - } - } - - SORT_AND_UNIQUE(c_ids); - - for (content_t c_id : c_ids) - map[c_id].push_back(lbm_def); -} - -const LBMContentMapping::lbm_vector * -LBMContentMapping::lookup(content_t c) const -{ - lbm_map::const_iterator it = map.find(c); - if (it == map.end()) - return NULL; - // This first dereferences the iterator, returning - // a std::vector - // reference, then we convert it to a pointer. - return &(it->second); -} - -LBMManager::~LBMManager() -{ - for (auto &m_lbm_def : m_lbm_defs) { - delete m_lbm_def.second; - } - - m_lbm_lookup.clear(); -} - -void LBMManager::addLBMDef(LoadingBlockModifierDef *lbm_def) -{ - // Precondition, in query mode the map isn't used anymore - FATAL_ERROR_IF(m_query_mode, - "attempted to modify LBMManager in query mode"); - - if (str_starts_with(lbm_def->name, ":")) - lbm_def->name.erase(0, 1); - - if (lbm_def->name.empty() || - !string_allowed(lbm_def->name, LBM_NAME_ALLOWED_CHARS)) { - throw ModError("Error adding LBM \"" + lbm_def->name + - "\": Does not follow naming conventions: " - "Only characters [a-z0-9_:] are allowed."); - } - - m_lbm_defs[lbm_def->name] = lbm_def; -} - -void LBMManager::loadIntroductionTimes(const std::string ×, - IGameDef *gamedef, u32 now) -{ - m_query_mode = true; - - auto introduction_times = parseIntroductionTimesString(times); - - // Put stuff from introduction_times into m_lbm_lookup - for (auto &[name, time] : introduction_times) { - auto def_it = m_lbm_defs.find(name); - if (def_it == m_lbm_defs.end()) { - infostream << "LBMManager: LBM " << name << " is not registered. " - "Discarding it." << std::endl; - continue; - } - auto *lbm_def = def_it->second; - if (lbm_def->run_at_every_load) { - continue; // These are handled below - } - if (time > now) { - warningstream << "LBMManager: LBM " << name << " was introduced in " - "the future. Pretending it's new." << std::endl; - // By skipping here it will be added as newly introduced. - continue; - } - - m_lbm_lookup[time].addLBM(lbm_def, gamedef); - - // Erase the entry so that we know later - // which elements didn't get put into m_lbm_lookup - m_lbm_defs.erase(def_it); - } - - // Now also add the elements from m_lbm_defs to m_lbm_lookup - // that weren't added in the previous step. - // They are introduced first time to this world, - // or are run at every load (introduction time hardcoded to U32_MAX). - - auto &lbms_we_introduce_now = m_lbm_lookup[now]; - auto &lbms_running_always = m_lbm_lookup[U32_MAX]; - for (auto &it : m_lbm_defs) { - if (it.second->run_at_every_load) - lbms_running_always.addLBM(it.second, gamedef); - else - lbms_we_introduce_now.addLBM(it.second, gamedef); - } - - // All pointer ownership now moved to LBMContentMapping - m_lbm_defs.clear(); - - // If these are empty delete them again to avoid pointless iteration. - if (lbms_we_introduce_now.empty()) - m_lbm_lookup.erase(now); - if (lbms_running_always.empty()) - m_lbm_lookup.erase(U32_MAX); - - infostream << "LBMManager: " << m_lbm_lookup.size() << - " unique times in lookup table" << std::endl; -} - -std::string LBMManager::createIntroductionTimesString() -{ - // Precondition, we must be in query mode - FATAL_ERROR_IF(!m_query_mode, - "attempted to query on non fully set up LBMManager"); - - std::ostringstream oss; - for (const auto &it : m_lbm_lookup) { - u32 time = it.first; - auto &lbm_list = it.second.getList(); - for (const auto &lbm_def : lbm_list) { - // Don't add if the LBM runs at every load, - // then introduction time is hardcoded and doesn't need to be stored. - if (lbm_def->run_at_every_load) - continue; - oss << lbm_def->name << "~" << time << ";"; - } - } - return oss.str(); -} - -std::unordered_map - LBMManager::parseIntroductionTimesString(const std::string ×) -{ - std::unordered_map ret; - - size_t idx = 0; - size_t idx_new; - while ((idx_new = times.find(';', idx)) != std::string::npos) { - std::string entry = times.substr(idx, idx_new - idx); - idx = idx_new + 1; - - std::vector components = str_split(entry, '~'); - if (components.size() != 2) - throw SerializationError("Introduction times entry \"" - + entry + "\" requires exactly one '~'!"); - if (components[0].empty()) - throw SerializationError("LBM name is empty"); - std::string name = std::move(components[0]); - if (name.front() == ':') // old versions didn't strip this - name.erase(0, 1); - u32 time = from_string(components[1]); - ret[std::move(name)] = time; - } - - return ret; -} - -namespace { - struct LBMToRun { - std::unordered_set p; // node positions - std::vector l; // ordered list of LBMs - - template - void insertLBMs(const C &container) { - for (auto &it : container) { - if (!CONTAINS(l, it)) - l.push_back(it); - } - } - }; -} - -void LBMManager::applyLBMs(ServerEnvironment *env, MapBlock *block, - const u32 stamp, const float dtime_s) -{ - // Precondition, we need m_lbm_lookup to be initialized - FATAL_ERROR_IF(!m_query_mode, - "attempted to query on non fully set up LBMManager"); - - // Collect a list of all LBMs and associated positions - std::unordered_map to_run; - - // Note: the iteration count of this outer loop is typically very low, so it's ok. - for (auto it = getLBMsIntroducedAfter(stamp); it != m_lbm_lookup.end(); ++it) { - v3s16 pos; - content_t c; - - // Cache previous lookups since it has a high performance penalty. - content_t previous_c = CONTENT_IGNORE; - const LBMContentMapping::lbm_vector *lbm_list = nullptr; - LBMToRun *batch = nullptr; - - for (pos.Z = 0; pos.Z < MAP_BLOCKSIZE; pos.Z++) - for (pos.Y = 0; pos.Y < MAP_BLOCKSIZE; pos.Y++) - for (pos.X = 0; pos.X < MAP_BLOCKSIZE; pos.X++) { - c = block->getNodeNoCheck(pos).getContent(); - - bool c_changed = false; - if (previous_c != c) { - c_changed = true; - lbm_list = it->second.lookup(c); - if (lbm_list) - batch = &to_run[c]; // creates entry - previous_c = c; - } - - if (!lbm_list) - continue; - batch->p.insert(pos); - if (c_changed) { - batch->insertLBMs(*lbm_list); - } else { - // we were here before so the list must be filled - assert(!batch->l.empty()); - } - } - } - - // Actually run them - bool first = true; - for (auto &[c, batch] : to_run) { - if (tracestream) { - tracestream << "Running " << batch.l.size() << " LBMs for node " - << env->getGameDef()->ndef()->get(c).name << " (" - << batch.p.size() << "x) in block " << block->getPos() << std::endl; - } - for (auto &lbm_def : batch.l) { - if (!first) { - // The fun part: since any LBM call can change the nodes inside of he - // block, we have to recheck the positions to see if the wanted node - // is still there. - // Note that we don't rescan the whole block, we don't want to include new changes. - for (auto it2 = batch.p.begin(); it2 != batch.p.end(); ) { - if (block->getNodeNoCheck(*it2).getContent() != c) - it2 = batch.p.erase(it2); - else - ++it2; - } - } else { - assert(!batch.p.empty()); - } - first = false; - - if (batch.p.empty()) - break; - lbm_def->trigger(env, block, batch.p, dtime_s); - if (block->isOrphan()) - return; - } - } -} /* ActiveBlockList @@ -839,252 +538,6 @@ void ServerEnvironment::loadDefaultMeta() m_lbm_mgr.loadIntroductionTimes("", m_server, m_game_time); } -struct ActiveABM -{ - ActiveBlockModifier *abm; - std::vector required_neighbors; - std::vector without_neighbors; - int chance; - s16 min_y, max_y; -}; - -#define CONTENT_TYPE_CACHE_MAX 64 - -class ABMHandler -{ -private: - ServerEnvironment *m_env; - std::vector *> m_aabms; -public: - ABMHandler(std::vector &abms, - float dtime_s, ServerEnvironment *env, - bool use_timers): - m_env(env) - { - if (dtime_s < 0.001f) - return; - const NodeDefManager *ndef = env->getGameDef()->ndef(); - for (ABMWithState &abmws : abms) { - ActiveBlockModifier *abm = abmws.abm; - float trigger_interval = abm->getTriggerInterval(); - if (trigger_interval < 0.001f) - trigger_interval = 0.001f; - float actual_interval = dtime_s; - if (use_timers) { - abmws.timer += dtime_s; - if(abmws.timer < trigger_interval) - continue; - abmws.timer -= trigger_interval; - actual_interval = trigger_interval; - } - float chance = abm->getTriggerChance(); - if (chance == 0) - chance = 1; - - ActiveABM aabm; - aabm.abm = abm; - if (abm->getSimpleCatchUp()) { - float intervals = actual_interval / trigger_interval; - if (intervals == 0) - continue; - aabm.chance = chance / intervals; - if (aabm.chance == 0) - aabm.chance = 1; - } else { - aabm.chance = chance; - } - // y limits - aabm.min_y = abm->getMinY(); - aabm.max_y = abm->getMaxY(); - - // Trigger neighbors - for (const auto &s : abm->getRequiredNeighbors()) - ndef->getIds(s, aabm.required_neighbors); - SORT_AND_UNIQUE(aabm.required_neighbors); - - for (const auto &s : abm->getWithoutNeighbors()) - ndef->getIds(s, aabm.without_neighbors); - SORT_AND_UNIQUE(aabm.without_neighbors); - - // Trigger contents - std::vector ids; - for (const auto &s : abm->getTriggerContents()) - ndef->getIds(s, ids); - SORT_AND_UNIQUE(ids); - for (content_t c : ids) { - if (c >= m_aabms.size()) - m_aabms.resize(c + 256, nullptr); - if (!m_aabms[c]) - m_aabms[c] = new std::vector; - m_aabms[c]->push_back(aabm); - } - } - } - - ~ABMHandler() - { - for (auto &aabms : m_aabms) - delete aabms; - } - - // Find out how many objects the given block and its neighbors contain. - // Returns the number of objects in the block, and also in 'wider' the - // number of objects in the block and all its neighbors. The latter - // may an estimate if any neighbors are unloaded. - u32 countObjects(MapBlock *block, ServerMap * map, u32 &wider) - { - wider = 0; - u32 wider_unknown_count = 0; - for(s16 x=-1; x<=1; x++) - for(s16 y=-1; y<=1; y++) - for(s16 z=-1; z<=1; z++) - { - MapBlock *block2 = map->getBlockNoCreateNoEx( - block->getPos() + v3s16(x,y,z)); - if(block2==NULL){ - wider_unknown_count++; - continue; - } - wider += block2->m_static_objects.size(); - } - // Extrapolate - u32 active_object_count = block->m_static_objects.getActiveSize(); - u32 wider_known_count = 3 * 3 * 3 - wider_unknown_count; - wider += wider_unknown_count * wider / wider_known_count; - return active_object_count; - } - void apply(MapBlock *block, int &blocks_scanned, int &abms_run, int &blocks_cached) - { - if (m_aabms.empty()) - return; - - // Check the content type cache first - // to see whether there are any ABMs - // to be run at all for this block. - if (!block->contents.empty()) { - assert(!block->do_not_cache_contents); // invariant - blocks_cached++; - bool run_abms = false; - for (content_t c : block->contents) { - if (c < m_aabms.size() && m_aabms[c]) { - run_abms = true; - break; - } - } - if (!run_abms) - return; - } - blocks_scanned++; - - ServerMap *map = &m_env->getServerMap(); - - u32 active_object_count_wider; - u32 active_object_count = this->countObjects(block, map, active_object_count_wider); - m_env->m_added_objects = 0; - - bool want_contents_cached = block->contents.empty() && !block->do_not_cache_contents; - - v3s16 p0; - for(p0.Z=0; p0.ZgetNodeNoCheck(p0); - content_t c = n.getContent(); - - // Cache content types as we go - if (want_contents_cached && !CONTAINS(block->contents, c)) { - if (block->contents.size() >= CONTENT_TYPE_CACHE_MAX) { - // Too many different nodes... don't try to cache - want_contents_cached = false; - block->do_not_cache_contents = true; - block->contents.clear(); - block->contents.shrink_to_fit(); - } else { - block->contents.push_back(c); - } - } - - if (c >= m_aabms.size() || !m_aabms[c]) - continue; - - v3s16 p = p0 + block->getPosRelative(); - for (ActiveABM &aabm : *m_aabms[c]) { - if ((p.Y < aabm.min_y) || (p.Y > aabm.max_y)) - continue; - - if (myrand() % aabm.chance != 0) - continue; - - // Check neighbors - const bool check_required_neighbors = !aabm.required_neighbors.empty(); - const bool check_without_neighbors = !aabm.without_neighbors.empty(); - if (check_required_neighbors || check_without_neighbors) { - v3s16 p1; - bool have_required = false; - for(p1.X = p0.X-1; p1.X <= p0.X+1; p1.X++) - for(p1.Y = p0.Y-1; p1.Y <= p0.Y+1; p1.Y++) - for(p1.Z = p0.Z-1; p1.Z <= p0.Z+1; p1.Z++) - { - if(p1 == p0) - continue; - content_t c; - if (block->isValidPosition(p1)) { - // if the neighbor is found on the same map block - // get it straight from there - const MapNode &n = block->getNodeNoCheck(p1); - c = n.getContent(); - } else { - // otherwise consult the map - MapNode n = map->getNode(p1 + block->getPosRelative()); - c = n.getContent(); - } - if (check_required_neighbors && !have_required) { - if (CONTAINS(aabm.required_neighbors, c)) { - if (!check_without_neighbors) - goto neighbor_found; - have_required = true; - } - } - if (check_without_neighbors) { - if (CONTAINS(aabm.without_neighbors, c)) - goto neighbor_invalid; - } - } - if (have_required || !check_required_neighbors) - goto neighbor_found; - // No required neighbor found - neighbor_invalid: - continue; - } - - neighbor_found: - - abms_run++; - // Call all the trigger variations - aabm.abm->trigger(m_env, p, n); - aabm.abm->trigger(m_env, p, n, - active_object_count, active_object_count_wider); - - if (block->isOrphan()) - return; - - // Count surrounding objects again if the abms added any - if(m_env->m_added_objects > 0) { - active_object_count = countObjects(block, map, active_object_count_wider); - m_env->m_added_objects = 0; - } - - // Update and check node after possible modification - n = block->getNodeNoCheck(p0); - if (n.getContent() != c) - break; - } - } - } -}; - - void ServerEnvironment::forceActivateBlock(MapBlock *block) { assert(block); diff --git a/src/serverenvironment.h b/src/serverenvironment.h index 267f6af83..c7396987a 100644 --- a/src/serverenvironment.h +++ b/src/serverenvironment.h @@ -12,6 +12,7 @@ #include "servermap.h" #include "settings.h" #include "server/activeobjectmgr.h" +#include "server/blockmodifier.h" #include "util/numeric.h" #include "util/metricsbackend.h" @@ -22,7 +23,6 @@ class PlayerDatabase; class AuthDatabase; class PlayerSAO; class ServerEnvironment; -class ActiveBlockModifier; struct StaticObject; class ServerActiveObject; class Server; @@ -30,138 +30,6 @@ class ServerScripting; enum AccessDeniedCode : u8; typedef u16 session_t; -/* - {Active, Loading} block modifier interface. - - These are fed into ServerEnvironment at initialization time; - ServerEnvironment handles deleting them. -*/ - -class ActiveBlockModifier -{ -public: - ActiveBlockModifier() = default; - virtual ~ActiveBlockModifier() = default; - - // Set of contents to trigger on - virtual const std::vector &getTriggerContents() const = 0; - // Set of required neighbors (trigger doesn't happen if none are found) - // Empty = do not check neighbors - virtual const std::vector &getRequiredNeighbors() const = 0; - // Set of without neighbors (trigger doesn't happen if any are found) - // Empty = do not check neighbors - virtual const std::vector &getWithoutNeighbors() const = 0; - // Trigger interval in seconds - virtual float getTriggerInterval() = 0; - // Random chance of (1 / return value), 0 is disallowed - virtual u32 getTriggerChance() = 0; - // Whether to modify chance to simulate time lost by an unnattended block - virtual bool getSimpleCatchUp() = 0; - // get min Y for apply abm - virtual s16 getMinY() = 0; - // get max Y for apply abm - virtual s16 getMaxY() = 0; - // This is called usually at interval for 1/chance of the nodes - virtual void trigger(ServerEnvironment *env, v3s16 p, MapNode n){}; - virtual void trigger(ServerEnvironment *env, v3s16 p, MapNode n, - u32 active_object_count, u32 active_object_count_wider){}; -}; - -struct ABMWithState -{ - ActiveBlockModifier *abm; - float timer = 0.0f; - - ABMWithState(ActiveBlockModifier *abm_); -}; - -struct LoadingBlockModifierDef -{ - // Set of contents to trigger on - std::vector trigger_contents; - std::string name; - bool run_at_every_load = false; - - virtual ~LoadingBlockModifierDef() = default; - - /// @brief Called to invoke LBM - /// @param env environment - /// @param block the block in question - /// @param positions set of node positions (block-relative!) - /// @param dtime_s game time since last deactivation - virtual void trigger(ServerEnvironment *env, MapBlock *block, - const std::unordered_set &positions, float dtime_s) {}; -}; - -class LBMContentMapping -{ -public: - typedef std::vector lbm_vector; - typedef std::unordered_map lbm_map; - - LBMContentMapping() = default; - void addLBM(LoadingBlockModifierDef *lbm_def, IGameDef *gamedef); - const lbm_map::mapped_type *lookup(content_t c) const; - const lbm_vector &getList() const { return lbm_list; } - bool empty() const { return lbm_list.empty(); } - - // This struct owns the LBM pointers. - ~LBMContentMapping(); - DISABLE_CLASS_COPY(LBMContentMapping); - ALLOW_CLASS_MOVE(LBMContentMapping); - -private: - lbm_vector lbm_list; - lbm_map map; -}; - -class LBMManager -{ -public: - LBMManager() = default; - ~LBMManager(); - - // Don't call this after loadIntroductionTimes() ran. - void addLBMDef(LoadingBlockModifierDef *lbm_def); - - /// @param now current game time - void loadIntroductionTimes(const std::string ×, - IGameDef *gamedef, u32 now); - - // Don't call this before loadIntroductionTimes() ran. - std::string createIntroductionTimesString(); - - // Don't call this before loadIntroductionTimes() ran. - void applyLBMs(ServerEnvironment *env, MapBlock *block, - u32 stamp, float dtime_s); - - // Warning: do not make this std::unordered_map, order is relevant here - typedef std::map lbm_lookup_map; - -private: - // Once we set this to true, we can only query, - // not modify - bool m_query_mode = false; - - // For m_query_mode == false: - // The key of the map is the LBM def's name. - std::unordered_map m_lbm_defs; - - // For m_query_mode == true: - // The key of the map is the LBM def's first introduction time. - lbm_lookup_map m_lbm_lookup; - - /// @return map of LBM name -> timestamp - static std::unordered_map - parseIntroductionTimesString(const std::string ×); - - // Returns an iterator to the LBMs that were introduced - // after the given time. This is guaranteed to return - // valid values for everything - lbm_lookup_map::const_iterator getLBMsIntroducedAfter(u32 time) - { return m_lbm_lookup.lower_bound(time); } -}; - /* List of active blocks, used by ServerEnvironment */ diff --git a/src/unittest/test_lbmmanager.cpp b/src/unittest/test_lbmmanager.cpp index 6f3627331..d4490bd01 100644 --- a/src/unittest/test_lbmmanager.cpp +++ b/src/unittest/test_lbmmanager.cpp @@ -6,7 +6,7 @@ #include -#include "serverenvironment.h" +#include "server/blockmodifier.h" class TestLBMManager : public TestBase {