From 6ed1dab3fb338954544801fc86bd1f8d97e7afb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Thu, 8 Jun 2023 19:04:44 +0200 Subject: [PATCH 01/16] Add random resource to RMG pool --- config/objects/moddables.json | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/config/objects/moddables.json b/config/objects/moddables.json index 93f1db2a4..643b66dcb 100644 --- a/config/objects/moddables.json +++ b/config/objects/moddables.json @@ -50,6 +50,34 @@ } }, + "randomResource": + { + "index" :76, + "handler": "resource", + "base" : { + "base" : { + "visitableFrom" : [ "+++", "+-+", "+++" ], + "mask" : [ "VA" ] + } + }, + "types" : { + "randomResource" : { + "index" : 0, + "rmg" : { + "value" : 1500, + "rarity" : 2000 + }, + "templates" : + { + "res" : + { + "animation" : "AVTrndm0.def" + } + } + } + } + }, + // subtype: resource ID "resource" : { "index" :79, From d92ac6bcbeb7a802f0bb8eeff0f9d94177a3a9ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Thu, 8 Jun 2023 19:23:23 +0200 Subject: [PATCH 02/16] Do not exceed rolled treasure value - matches OH3. --- lib/rmg/modificators/TreasurePlacer.cpp | 6 +++--- lib/rmg/modificators/TreasurePlacer.h | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/rmg/modificators/TreasurePlacer.cpp b/lib/rmg/modificators/TreasurePlacer.cpp index 4ac80b42e..2fd18de25 100644 --- a/lib/rmg/modificators/TreasurePlacer.cpp +++ b/lib/rmg/modificators/TreasurePlacer.cpp @@ -562,7 +562,7 @@ std::vector TreasurePlacer::prepareTreasurePile(const CTreasureInfo bool hasLargeObject = false; while(currentValue <= static_cast(desiredValue) - 100) //no objects with value below 100 are available { - auto * oi = getRandomObject(desiredValue, currentValue, maxValue, !hasLargeObject); + auto * oi = getRandomObject(desiredValue, currentValue, !hasLargeObject); if(!oi) //fail break; @@ -659,13 +659,13 @@ rmg::Object TreasurePlacer::constructTreasurePile(const std::vector return rmgObject; } -ObjectInfo * TreasurePlacer::getRandomObject(ui32 desiredValue, ui32 currentValue, ui32 maxValue, bool allowLargeObjects) +ObjectInfo * TreasurePlacer::getRandomObject(ui32 desiredValue, ui32 currentValue, bool allowLargeObjects) { std::vector> thresholds; //handle complex object via pointer ui32 total = 0; //calculate actual treasure value range based on remaining value - ui32 maxVal = maxValue - currentValue; + ui32 maxVal = desiredValue - currentValue; ui32 minValue = static_cast(0.25f * (desiredValue - currentValue)); for(ObjectInfo & oi : possibleObjects) //copy constructor turned out to be costly diff --git a/lib/rmg/modificators/TreasurePlacer.h b/lib/rmg/modificators/TreasurePlacer.h index edef4581b..822bc793b 100644 --- a/lib/rmg/modificators/TreasurePlacer.h +++ b/lib/rmg/modificators/TreasurePlacer.h @@ -54,7 +54,7 @@ public: protected: bool isGuardNeededForTreasure(int value); - ObjectInfo * getRandomObject(ui32 desiredValue, ui32 currentValue, ui32 maxValue, bool allowLargeObjects); + ObjectInfo * getRandomObject(ui32 desiredValue, ui32 currentValue, bool allowLargeObjects); std::vector prepareTreasurePile(const CTreasureInfo & treasureInfo); rmg::Object constructTreasurePile(const std::vector & treasureInfos, bool densePlacement = false); From 66b6fba51f8bdbba576c5c0c98159cb1c2105e13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Thu, 8 Jun 2023 19:31:38 +0200 Subject: [PATCH 03/16] Use all tiles covered by treasure pile to determine distance to other treasures. --- lib/rmg/modificators/ObjectManager.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/rmg/modificators/ObjectManager.cpp b/lib/rmg/modificators/ObjectManager.cpp index 52c3eb8ce..5b7b051c8 100644 --- a/lib/rmg/modificators/ObjectManager.cpp +++ b/lib/rmg/modificators/ObjectManager.cpp @@ -183,8 +183,15 @@ int3 ObjectManager::findPlaceForObject(const rmg::Area & searchArea, rmg::Object for(const auto & t : obj.getArea().getTilesVector()) { - if(map.getTileInfo(t).getNearestObjectDistance() < min_dist) + auto localDist = map.getTileInfo(t).getNearestObjectDistance(); + if (localDist < min_dist) + { return -1.f; + } + else + { + vstd::amin(dist, localDist); //Evaluate object tile which will be closest to another object + } } return dist; From 52d33fc7a6d6e2653f9347691526ee4592d59772 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Thu, 8 Jun 2023 19:51:21 +0200 Subject: [PATCH 04/16] Generate all treasures of certain value beforehand and try to place them all, don't interrupt at first failure. --- lib/rmg/RmgObject.cpp | 29 +++- lib/rmg/RmgObject.h | 4 + lib/rmg/modificators/TreasurePlacer.cpp | 187 ++++++++++++++---------- 3 files changed, 137 insertions(+), 83 deletions(-) diff --git a/lib/rmg/RmgObject.cpp b/lib/rmg/RmgObject.cpp index bfe775f1c..540463948 100644 --- a/lib/rmg/RmgObject.cpp +++ b/lib/rmg/RmgObject.cpp @@ -161,17 +161,21 @@ const CGObjectInstance & Object::Instance::object() const return dObject; } -Object::Object(CGObjectInstance & object, const int3 & position) +Object::Object(CGObjectInstance & object, const int3 & position): + guarded(false) { addInstance(object, position); } -Object::Object(CGObjectInstance & object) +Object::Object(CGObjectInstance & object): + guarded(false) { addInstance(object); } -Object::Object(const Object & object): dStrength(object.dStrength) +Object::Object(const Object & object): + dStrength(object.dStrength), + guarded(false) { for(const auto & i : object.dInstances) addInstance(const_cast(i.object()), i.getPosition()); @@ -197,7 +201,9 @@ std::list Object::instances() const void Object::addInstance(Instance & object) { //assert(object.dParent == *this); + setGuardedIfMonster(object); dInstances.push_back(object); + dFullAreaCache.clear(); dAccessibleAreaCache.clear(); dAccessibleAreaFullCache.clear(); @@ -206,6 +212,8 @@ void Object::addInstance(Instance & object) Object::Instance & Object::addInstance(CGObjectInstance & object) { dInstances.emplace_back(*this, object); + setGuardedIfMonster(dInstances.back()); + dFullAreaCache.clear(); dAccessibleAreaCache.clear(); dAccessibleAreaFullCache.clear(); @@ -215,6 +223,8 @@ Object::Instance & Object::addInstance(CGObjectInstance & object) Object::Instance & Object::addInstance(CGObjectInstance & object, const int3 & position) { dInstances.emplace_back(*this, object, position); + setGuardedIfMonster(dInstances.back()); + dFullAreaCache.clear(); dAccessibleAreaCache.clear(); dAccessibleAreaFullCache.clear(); @@ -302,6 +312,19 @@ const int3 Object::getVisibleTop() const return topTile; } +bool rmg::Object::isGuarded() const +{ + return guarded; +} + +void rmg::Object::setGuardedIfMonster(const Instance& object) +{ + if (object.object().ID == Obj::MONSTER) + { + guarded = true; + } +} + void Object::Instance::finalize(RmgMap & map) { if(!map.isOnMap(getPosition(true))) diff --git a/lib/rmg/RmgObject.h b/lib/rmg/RmgObject.h index a164b9161..d444a90b4 100644 --- a/lib/rmg/RmgObject.h +++ b/lib/rmg/RmgObject.h @@ -77,6 +77,9 @@ public: const Area & getArea() const; //lazy cache invalidation const int3 getVisibleTop() const; + + bool isGuarded() const; + void setGuardedIfMonster(const Instance & object); void finalize(RmgMap & map); void clear(); @@ -87,6 +90,7 @@ private: mutable Area dAccessibleAreaCache, dAccessibleAreaFullCache; int3 dPosition; ui32 dStrength; + bool guarded; }; } diff --git a/lib/rmg/modificators/TreasurePlacer.cpp b/lib/rmg/modificators/TreasurePlacer.cpp index 2fd18de25..f39c5107b 100644 --- a/lib/rmg/modificators/TreasurePlacer.cpp +++ b/lib/rmg/modificators/TreasurePlacer.cpp @@ -701,133 +701,160 @@ ObjectInfo * TreasurePlacer::getRandomObject(ui32 desiredValue, ui32 currentValu } } -void TreasurePlacer::createTreasures(ObjectManager & manager) +void TreasurePlacer::createTreasures(ObjectManager& manager) { const int maxAttempts = 2; - + const int minDistance = 2; + int mapMonsterStrength = map.getMapGenOptions().getMonsterStrength(); - int monsterStrength = (zone.monsterStrength == EMonsterStrength::ZONE_NONE ? 0 : zone.monsterStrength + mapMonsterStrength - 1); //array index from 0 to 4; pick any correct value for ZONE_NONE, minGuardedValue won't be used in this case anyway + int monsterStrength = (zone.monsterStrength == EMonsterStrength::ZONE_NONE ? 0 : zone.monsterStrength + mapMonsterStrength - 1); //array index from 0 to 4; pick any correct value for ZONE_NONE, minGuardedValue won't be used in this case anyway static int minGuardedValues[] = { 6500, 4167, 3000, 1833, 1333 }; minGuardedValue = minGuardedValues[monsterStrength]; - - auto valueComparator = [](const CTreasureInfo & lhs, const CTreasureInfo & rhs) -> bool + + auto valueComparator = [](const CTreasureInfo& lhs, const CTreasureInfo& rhs) -> bool { return lhs.max > rhs.max; }; - - auto restoreZoneLimits = [](const std::vector & treasurePile) + + auto restoreZoneLimits = [](const std::vector& treasurePile) { - for(auto * oi : treasurePile) + for (auto* oi : treasurePile) { oi->maxPerZone++; } }; - + //place biggest treasures first at large distance, place smaller ones inbetween auto treasureInfo = zone.getTreasureInfo(); boost::sort(treasureInfo, valueComparator); - + //sort treasures by ascending value so we can stop checking treasures with too high value boost::sort(possibleObjects, [](const ObjectInfo& oi1, const ObjectInfo& oi2) -> bool { return oi1.value < oi2.value; }); - + + size_t size = 0; + { + Zone::Lock lock(zone.areaMutex); + size = zone.areaPossible().getTiles().size(); + } + int totalDensity = 0; + for (auto t : treasureInfo) { + std::vector treasures; + //discard objects with too high value to be ever placed vstd::erase_if(possibleObjects, [t](const ObjectInfo& oi) -> bool { return oi.value > t.max; }); - + totalDensity += t.density; - + + const size_t count = size * t.density / 300; + //treasure density is inversely proportional to zone size but must be scaled back to map size //also, normalize it to zone count - higher count means relatively smaller zones //this is squared distance for optimization purposes - const float minDistance = std::max((125.f / totalDensity), 2.0f); - //distance lower than 2 causes objects to overlap and crash - - for(int attempt = 0; attempt <= maxAttempts;) + const float minDistance = std::max((125.f / totalDensity), 1.0f); + + for (size_t i = 0; i < count;) { auto treasurePileInfos = prepareTreasurePile(t); - if(treasurePileInfos.empty()) + if (treasurePileInfos.empty()) { - ++attempt; continue; } - - int value = std::accumulate(treasurePileInfos.begin(), treasurePileInfos.end(), 0, [](int v, const ObjectInfo * oi){return v + oi->value;}); - - auto rmgObject = constructTreasurePile(treasurePileInfos, attempt == maxAttempts); - if(rmgObject.instances().empty()) //handle incorrect placement + else { - restoreZoneLimits(treasurePileInfos); - continue; + i++; } - - //guard treasure pile - bool guarded = isGuardNeededForTreasure(value); - if(guarded) - guarded = manager.addGuard(rmgObject, value); - - Zone::Lock lock(zone.areaMutex); //We are going to subtract this area - //TODO: Don't place - auto possibleArea = zone.areaPossible(); - - auto path = rmg::Path::invalid(); - if(guarded) - { - path = manager.placeAndConnectObject(possibleArea, rmgObject, [this, &rmgObject, &minDistance, &manager](const int3 & tile) - { - auto ti = map.getTileInfo(tile); - if(ti.getNearestObjectDistance() < minDistance) - return -1.f; - for(const auto & t : rmgObject.getArea().getTilesVector()) - { - if(map.getTileInfo(t).getNearestObjectDistance() < minDistance) - return -1.f; - } - - auto guardedArea = rmgObject.instances().back()->getAccessibleArea(); - auto areaToBlock = rmgObject.getAccessibleArea(true); - areaToBlock.subtract(guardedArea); - if(areaToBlock.overlap(zone.freePaths()) || areaToBlock.overlap(manager.getVisitableArea())) - return -1.f; - - return ti.getNearestObjectDistance(); - }, guarded, false, ObjectManager::OptimizeType::DISTANCE); - } - else + int value = std::accumulate(treasurePileInfos.begin(), treasurePileInfos.end(), 0, [](int v, const ObjectInfo* oi) {return v + oi->value; }); + + for (ui32 attempt = 0; attempt <= 2; attempt++) { - path = manager.placeAndConnectObject(possibleArea, rmgObject, minDistance, guarded, false, ObjectManager::OptimizeType::DISTANCE); - } - - if(path.valid()) - { - //debug purposes - treasureArea.unite(rmgObject.getArea()); - if(guarded) + auto rmgObject = constructTreasurePile(treasurePileInfos, attempt == maxAttempts); + + if (rmgObject.instances().empty()) //handle incorrect placement { - guards.unite(rmgObject.instances().back()->getBlockedArea()); - auto guardedArea = rmgObject.instances().back()->getAccessibleArea(); - auto areaToBlock = rmgObject.getAccessibleArea(true); - areaToBlock.subtract(guardedArea); - treasureBlockArea.unite(areaToBlock); + restoreZoneLimits(treasurePileInfos); + continue; } - zone.connectPath(path); - manager.placeObject(rmgObject, guarded, true); - attempt = 0; + + //guard treasure pile + bool guarded = isGuardNeededForTreasure(value); + if (guarded) + guarded = manager.addGuard(rmgObject, value); + + treasures.push_back(rmgObject); + break; } - else + } + + for (auto& rmgObject : treasures) + { + const bool guarded = rmgObject.isGuarded(); + + for (int attempt = 0; attempt <= maxAttempts;) { - restoreZoneLimits(treasurePileInfos); - rmgObject.clear(); - ++attempt; + auto path = rmg::Path::invalid(); + + Zone::Lock lock(zone.areaMutex); //We are going to subtract this area + auto possibleArea = zone.areaPossible(); + + if (guarded) + { + path = manager.placeAndConnectObject(possibleArea, rmgObject, [this, &rmgObject, &minDistance, &manager](const int3& tile) + { + auto ti = map.getTileInfo(tile); + if (ti.getNearestObjectDistance() < minDistance) + return -1.f; + + for (const auto& t : rmgObject.getArea().getTilesVector()) + { + if (map.getTileInfo(t).getNearestObjectDistance() < minDistance) + return -1.f; + } + + auto guardedArea = rmgObject.instances().back()->getAccessibleArea(); + auto areaToBlock = rmgObject.getAccessibleArea(true); + areaToBlock.subtract(guardedArea); + if (areaToBlock.overlap(zone.freePaths()) || areaToBlock.overlap(manager.getVisitableArea())) + return -1.f; + + return ti.getNearestObjectDistance(); + }, guarded, false, ObjectManager::OptimizeType::DISTANCE); + } + else + { + path = manager.placeAndConnectObject(possibleArea, rmgObject, minDistance, guarded, false, ObjectManager::OptimizeType::DISTANCE); + } + + if (path.valid()) + { + //debug purposes + treasureArea.unite(rmgObject.getArea()); + if (guarded) + { + guards.unite(rmgObject.instances().back()->getBlockedArea()); + auto guardedArea = rmgObject.instances().back()->getAccessibleArea(); + auto areaToBlock = rmgObject.getAccessibleArea(true); + areaToBlock.subtract(guardedArea); + treasureBlockArea.unite(areaToBlock); + } + zone.connectPath(path); + manager.placeObject(rmgObject, guarded, true); + break; + } + else + { + ++attempt; + } } } } From 682e9ae297409c7d85ef12e511f91a0d825c6de3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Thu, 8 Jun 2023 20:05:51 +0200 Subject: [PATCH 05/16] Unused variable --- lib/rmg/modificators/TreasurePlacer.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/rmg/modificators/TreasurePlacer.cpp b/lib/rmg/modificators/TreasurePlacer.cpp index f39c5107b..0f575a401 100644 --- a/lib/rmg/modificators/TreasurePlacer.cpp +++ b/lib/rmg/modificators/TreasurePlacer.cpp @@ -704,7 +704,6 @@ ObjectInfo * TreasurePlacer::getRandomObject(ui32 desiredValue, ui32 currentValu void TreasurePlacer::createTreasures(ObjectManager& manager) { const int maxAttempts = 2; - const int minDistance = 2; int mapMonsterStrength = map.getMapGenOptions().getMonsterStrength(); int monsterStrength = (zone.monsterStrength == EMonsterStrength::ZONE_NONE ? 0 : zone.monsterStrength + mapMonsterStrength - 1); //array index from 0 to 4; pick any correct value for ZONE_NONE, minGuardedValue won't be used in this case anyway From d5b799278ed22c6c6d38e8201587fe584c1229f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Fri, 9 Jun 2023 21:22:44 +0200 Subject: [PATCH 06/16] Allow center Town touch the blocked area, it's no longer a problem with correct pathfinder. --- lib/rmg/modificators/TownPlacer.cpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/rmg/modificators/TownPlacer.cpp b/lib/rmg/modificators/TownPlacer.cpp index f4e4f6895..883508c2c 100644 --- a/lib/rmg/modificators/TownPlacer.cpp +++ b/lib/rmg/modificators/TownPlacer.cpp @@ -163,11 +163,14 @@ void TownPlacer::cleanupBoundaries(const rmg::Object & rmgObject) Zone::Lock lock(zone.areaMutex); for(const auto & t : rmgObject.getArea().getBorderOutside()) { - if(map.isOnMap(t)) + if (t.y > rmgObject.getVisitablePosition().y) //Line below the town { - map.setOccupied(t, ETileType::FREE); - zone.areaPossible().erase(t); - zone.freePaths().add(t); + if (map.isOnMap(t)) + { + map.setOccupied(t, ETileType::FREE); + zone.areaPossible().erase(t); + zone.freePaths().add(t); + } } } } From 1bb2b5b57165a130eff9a5bb5f829d6bfa5b9c7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Sat, 10 Jun 2023 14:56:03 +0200 Subject: [PATCH 07/16] + Maintain clear perimeter of a treasure pile. + Make sure that separate blocked areas remain unconnected so it's possible to pass between them. --- lib/rmg/RmgArea.cpp | 22 ++++++++-- lib/rmg/RmgArea.h | 2 +- lib/rmg/modificators/ObjectManager.cpp | 52 +++++++++++++++++++++++- lib/rmg/modificators/ObstaclePlacer.cpp | 54 ++++++++++++++++++++++--- 4 files changed, 117 insertions(+), 13 deletions(-) diff --git a/lib/rmg/RmgArea.cpp b/lib/rmg/RmgArea.cpp index 5be26a8c4..3131bf188 100644 --- a/lib/rmg/RmgArea.cpp +++ b/lib/rmg/RmgArea.cpp @@ -64,21 +64,35 @@ void Area::invalidate() dBorderOutsideCache.clear(); } -bool Area::connected() const +bool Area::connected(bool noDiagonals) const { std::list queue({*dTiles.begin()}); Tileset connected = dTiles; //use invalidated cache - ok + while(!queue.empty()) { auto t = queue.front(); connected.erase(t); queue.pop_front(); - for(auto & i : int3::getDirs()) + if (noDiagonals) { - if(connected.count(t + i)) + for (auto& i : dirs4) { - queue.push_back(t + i); + if (connected.count(t + i)) + { + queue.push_back(t + i); + } + } + } + else + { + for (auto& i : int3::getDirs()) + { + if (connected.count(t + i)) + { + queue.push_back(t + i); + } } } } diff --git a/lib/rmg/RmgArea.h b/lib/rmg/RmgArea.h index 154c4acbf..1755f22d8 100644 --- a/lib/rmg/RmgArea.h +++ b/lib/rmg/RmgArea.h @@ -44,7 +44,7 @@ namespace rmg Area getSubarea(const std::function & filter) const; - bool connected() const; //is connected + bool connected(bool noDiagonals = false) const; //is connected bool empty() const; bool contains(const int3 & tile) const; bool contains(const std::vector & tiles) const; diff --git a/lib/rmg/modificators/ObjectManager.cpp b/lib/rmg/modificators/ObjectManager.cpp index 5b7b051c8..c82e632e0 100644 --- a/lib/rmg/modificators/ObjectManager.cpp +++ b/lib/rmg/modificators/ObjectManager.cpp @@ -166,7 +166,6 @@ int3 ObjectManager::findPlaceForObject(const rmg::Area & searchArea, rmg::Object } } - //FIXME: Race condition for tiles? For Area? if(result.valid()) obj.setPosition(result); return result; @@ -213,6 +212,55 @@ rmg::Path ObjectManager::placeAndConnectObject(const rmg::Area & searchArea, rmg return -1.f; } + rmg::Area perimeter; + rmg::Area areaToBlock; + if (obj.isGuarded()) + { + auto guardedArea = obj.instances().back()->getAccessibleArea(); + guardedArea.add(obj.instances().back()->getVisitablePosition()); + areaToBlock = obj.getAccessibleArea(true); + areaToBlock.subtract(guardedArea); + + if (!areaToBlock.empty()) + { + perimeter = areaToBlock; + perimeter.unite(areaToBlock.getBorderOutside()); + //We could have added border around guard + perimeter.subtract(guardedArea); + } + } + else + { + perimeter = obj.getArea(); + perimeter.subtract(obj.getAccessibleArea()); + if (!perimeter.empty()) + { + perimeter.unite(perimeter.getBorderOutside()); + perimeter.subtract(obj.getAccessibleArea()); + } + } + //Check if perimeter of the object intersects with more than one blocked areas + + auto tiles = perimeter.getTiles(); + vstd::erase_if(tiles, [this](const int3& tile) -> bool + { + //Out-of-map area also is an obstacle + if (!map.isOnMap(tile)) + return false; + return !(map.isBlocked(tile) || map.isUsed(tile)); + }); + + if (!tiles.empty()) + { + rmg::Area border(tiles); + border.subtract(areaToBlock); + if (!border.connected()) + { + //We don't want to connect two blocked areas to create impassable obstacle + return -1.f; + } + } + return dist; }, isGuarded, onlyStraight, optimizer); } @@ -237,7 +285,7 @@ rmg::Path ObjectManager::placeAndConnectObject(const rmg::Area & searchArea, rmg accessibleArea.intersect(guardedArea); accessibleArea.add(obj.instances().back()->getPosition(true)); } - + auto path = zone.searchPath(accessibleArea, onlyStraight, [&obj, isGuarded](const int3 & t) { if(isGuarded) diff --git a/lib/rmg/modificators/ObstaclePlacer.cpp b/lib/rmg/modificators/ObstaclePlacer.cpp index a19ffcb3f..62b58ec20 100644 --- a/lib/rmg/modificators/ObstaclePlacer.cpp +++ b/lib/rmg/modificators/ObstaclePlacer.cpp @@ -35,14 +35,56 @@ void ObstaclePlacer::process() collectPossibleObstacles(zone.getTerrainType()); - blockedArea = zone.area().getSubarea([this](const int3 & t) { - return map.shouldBeBlocked(t); - }); - blockedArea.subtract(zone.areaUsed()); - zone.areaPossible().subtract(blockedArea); + Zone::Lock lock(zone.areaMutex); + blockedArea = zone.area().getSubarea([this](const int3& t) + { + return map.shouldBeBlocked(t); + }); + blockedArea.subtract(zone.areaUsed()); + zone.areaPossible().subtract(blockedArea); - prohibitedArea = zone.freePaths() + zone.areaUsed() + manager->getVisitableArea(); + prohibitedArea = zone.freePaths() + zone.areaUsed() + manager->getVisitableArea(); + + //Progressively block tiles, but make sure they don't seal any gap between blocks + rmg::Area toBlock; + do + { + toBlock.clear(); + for (const auto& tile : zone.areaPossible().getTiles()) + { + rmg::Area neighbors; + rmg::Area t; + t.add(tile); + + for (const auto& n : t.getBorderOutside()) + { + //Area outside the map is also impassable + if (!map.isOnMap(n) || map.shouldBeBlocked(n)) + { + neighbors.add(n); + } + } + if (neighbors.empty()) + { + continue; + } + //Will only be added if it doesn't connect two disjointed blocks + if (neighbors.connected(true)) //Do not block diagonal pass + { + toBlock.add(tile); + } + } + zone.areaPossible().subtract(toBlock); + for (const auto& tile : toBlock.getTiles()) + { + map.setOccupied(tile, ETileType::BLOCKED); + } + + } while (!toBlock.empty()); + + prohibitedArea.unite(zone.areaPossible()); + } auto objs = createObstacles(zone.getRand()); mapProxy->insertObjects(objs); From b65870f31e5850bd8c6f9e11831bc6a26fafbb7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Sat, 10 Jun 2023 14:57:25 +0200 Subject: [PATCH 08/16] A bunch of magic formulas to assure nice balance of blocked areas and obstacles on every template --- lib/rmg/Zone.cpp | 30 +++++++++++++++++++++++-- lib/rmg/modificators/TreasurePlacer.cpp | 12 +++++----- 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/lib/rmg/Zone.cpp b/lib/rmg/Zone.cpp index ca0e3e159..eb459d993 100644 --- a/lib/rmg/Zone.cpp +++ b/lib/rmg/Zone.cpp @@ -203,8 +203,34 @@ void Zone::fractalize() rmg::Area possibleTiles(dAreaPossible); rmg::Area tilesToIgnore; //will be erased in this iteration - const float minDistance = 10 * 10; //squared - float blockDistance = minDistance * 0.25f; + //Squared + float minDistance = 10 * 10; + float spanFactor = (pos.z ? 0.25 : 0.5f); //Narrower passages in the Underground + + int treasureValue = 0; + int treasureDensity = 0; + for (auto t : treasureInfo) + { + treasureValue += ((t.min + t.max) / 2) * t.density / 1000.f; //Thousands + treasureDensity += t.density; + } + + if (treasureValue > 100) + { + //Less obstacles - max span is 1 (no obstacles) + spanFactor = 1.0f - ((std::max(0, (1000 - treasureValue)) / 1000.f) * (1 - spanFactor)); + } + else if (treasureValue < 100) + { + //Dense obstacles + spanFactor *= (treasureValue / 100.f); + vstd::amax(spanFactor, 0.2f); + } + if (treasureDensity <= 10) + { + vstd::amin(spanFactor, 0.25f); //Add extra obstacles to fill up space + } + float blockDistance = minDistance * spanFactor; //More obstacles in the Underground if(type != ETemplateZoneType::JUNCTION) { diff --git a/lib/rmg/modificators/TreasurePlacer.cpp b/lib/rmg/modificators/TreasurePlacer.cpp index 0f575a401..6ffed6e4c 100644 --- a/lib/rmg/modificators/TreasurePlacer.cpp +++ b/lib/rmg/modificators/TreasurePlacer.cpp @@ -736,7 +736,7 @@ void TreasurePlacer::createTreasures(ObjectManager& manager) size_t size = 0; { Zone::Lock lock(zone.areaMutex); - size = zone.areaPossible().getTiles().size(); + size = zone.getArea().getTiles().size(); } int totalDensity = 0; @@ -753,10 +753,12 @@ void TreasurePlacer::createTreasures(ObjectManager& manager) totalDensity += t.density; - const size_t count = size * t.density / 300; - - //treasure density is inversely proportional to zone size but must be scaled back to map size - //also, normalize it to zone count - higher count means relatively smaller zones + size_t count = size * t.density / 500; + const int averageValue = (t.min + t.max) / 2; + if (averageValue > 10000) //Will surely be guarded => larger + { + vstd::amin(count, size * (10.f/500) / (std::sqrt((float)averageValue / 10000))); + } //this is squared distance for optimization purposes const float minDistance = std::max((125.f / totalDensity), 1.0f); From 8ef25155dfd756156640d2ce8380a138c7201929 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Sat, 10 Jun 2023 14:58:12 +0200 Subject: [PATCH 09/16] Remove dependencies of surface and underground zones --- lib/rmg/modificators/ObstaclePlacer.cpp | 14 ++++++++++---- lib/rmg/modificators/RiverPlacer.cpp | 5 ++++- lib/rmg/modificators/RoadPlacer.cpp | 9 +++++++++ lib/rmg/modificators/RoadPlacer.h | 1 + lib/rmg/modificators/RockFiller.cpp | 2 -- lib/rmg/modificators/RockPlacer.cpp | 12 +++++++++++- lib/rmg/modificators/WaterProxy.cpp | 7 +++++-- 7 files changed, 40 insertions(+), 10 deletions(-) diff --git a/lib/rmg/modificators/ObstaclePlacer.cpp b/lib/rmg/modificators/ObstaclePlacer.cpp index 62b58ec20..26244f8da 100644 --- a/lib/rmg/modificators/ObstaclePlacer.cpp +++ b/lib/rmg/modificators/ObstaclePlacer.cpp @@ -12,7 +12,7 @@ #include "ObstaclePlacer.h" #include "ObjectManager.h" #include "TreasurePlacer.h" -#include "RockPlacer.h" +#include "RockFiller.h" #include "WaterRoutes.h" #include "WaterProxy.h" #include "RoadPlacer.h" @@ -94,10 +94,16 @@ void ObstaclePlacer::init() { DEPENDENCY(ObjectManager); DEPENDENCY(TreasurePlacer); - DEPENDENCY(WaterRoutes); - DEPENDENCY(WaterProxy); DEPENDENCY(RoadPlacer); - DEPENDENCY_ALL(RockPlacer); + if (zone.isUnderground()) + { + DEPENDENCY(RockFiller); + } + else + { + DEPENDENCY(WaterRoutes); + DEPENDENCY(WaterProxy); + } } bool ObstaclePlacer::isInTheMap(const int3& tile) diff --git a/lib/rmg/modificators/RiverPlacer.cpp b/lib/rmg/modificators/RiverPlacer.cpp index 901a5ec7e..5ce043c42 100644 --- a/lib/rmg/modificators/RiverPlacer.cpp +++ b/lib/rmg/modificators/RiverPlacer.cpp @@ -82,7 +82,10 @@ void RiverPlacer::process() void RiverPlacer::init() { - DEPENDENCY_ALL(WaterProxy); + if (!zone.isUnderground()) + { + DEPENDENCY_ALL(WaterProxy); + } DEPENDENCY(ObjectManager); DEPENDENCY(ObstaclePlacer); } diff --git a/lib/rmg/modificators/RoadPlacer.cpp b/lib/rmg/modificators/RoadPlacer.cpp index 2ecd74a89..dd9768fc1 100644 --- a/lib/rmg/modificators/RoadPlacer.cpp +++ b/lib/rmg/modificators/RoadPlacer.cpp @@ -12,6 +12,7 @@ #include "RoadPlacer.h" #include "ObjectManager.h" #include "ObstaclePlacer.h" +#include "RockFiller.h" #include "../Functions.h" #include "../CMapGenerator.h" #include "../threadpool/MapProxy.h" @@ -28,6 +29,14 @@ void RoadPlacer::process() connectRoads(); } +void RoadPlacer::init() +{ + if (zone.isUnderground()) + { + DEPENDENCY_ALL(RockFiller); + } +} + rmg::Area & RoadPlacer::areaForRoads() { return areaRoads; diff --git a/lib/rmg/modificators/RoadPlacer.h b/lib/rmg/modificators/RoadPlacer.h index 6ca5036c4..f7772e361 100644 --- a/lib/rmg/modificators/RoadPlacer.h +++ b/lib/rmg/modificators/RoadPlacer.h @@ -19,6 +19,7 @@ public: MODIFICATOR(RoadPlacer); void process() override; + void init(); char dump(const int3 &) override; void addRoadNode(const int3 & node); diff --git a/lib/rmg/modificators/RockFiller.cpp b/lib/rmg/modificators/RockFiller.cpp index 85c896280..9b0e0bf19 100644 --- a/lib/rmg/modificators/RockFiller.cpp +++ b/lib/rmg/modificators/RockFiller.cpp @@ -13,7 +13,6 @@ #include "RockPlacer.h" #include "TreasurePlacer.h" #include "ObjectManager.h" -#include "RoadPlacer.h" #include "RiverPlacer.h" #include "../RmgMap.h" #include "../CMapGenerator.h" @@ -63,7 +62,6 @@ void RockFiller::processMap() void RockFiller::init() { DEPENDENCY_ALL(RockPlacer); - POSTFUNCTION_ALL(RoadPlacer); } char RockFiller::dump(const int3 & t) diff --git a/lib/rmg/modificators/RockPlacer.cpp b/lib/rmg/modificators/RockPlacer.cpp index ba0360619..7056bdf0b 100644 --- a/lib/rmg/modificators/RockPlacer.cpp +++ b/lib/rmg/modificators/RockPlacer.cpp @@ -67,7 +67,17 @@ void RockPlacer::postProcess() void RockPlacer::init() { - DEPENDENCY_ALL(TreasurePlacer); + for (const auto& zone : map.getZones()) + { + if (zone.second->isUnderground()) + { + auto * tp = zone.second->getModificator(); + if (tp) + { + dependency(tp); + } + } + } } char RockPlacer::dump(const int3 & t) diff --git a/lib/rmg/modificators/WaterProxy.cpp b/lib/rmg/modificators/WaterProxy.cpp index a99489077..cf4e58684 100644 --- a/lib/rmg/modificators/WaterProxy.cpp +++ b/lib/rmg/modificators/WaterProxy.cpp @@ -83,8 +83,11 @@ void WaterProxy::init() { for(auto & z : map.getZones()) { - dependency(z.second->getModificator()); - dependency(z.second->getModificator()); + if (!zone.isUnderground()) + { + dependency(z.second->getModificator()); + dependency(z.second->getModificator()); + } postfunction(z.second->getModificator()); postfunction(z.second->getModificator()); } From f4dc8e6529eb52c925f8b07ed957a82ac2dc3275 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Sat, 10 Jun 2023 14:58:50 +0200 Subject: [PATCH 10/16] Limit morale / bonusing objects to one per zone --- config/objects/generic.json | 1 + config/objects/rewardableBonusing.json | 2 ++ 2 files changed, 3 insertions(+) diff --git a/config/objects/generic.json b/config/objects/generic.json index 0301f45a7..04c781969 100644 --- a/config/objects/generic.json +++ b/config/objects/generic.json @@ -511,6 +511,7 @@ "index" : 0, "aiValue" : 100, "rmg" : { + "zoneLimit" : 1, "mapLimit" : 32, "value" : 100, "rarity" : 20 diff --git a/config/objects/rewardableBonusing.json b/config/objects/rewardableBonusing.json index ff1f4c2cd..64f1855c0 100644 --- a/config/objects/rewardableBonusing.json +++ b/config/objects/rewardableBonusing.json @@ -15,6 +15,7 @@ "index" : 0, "aiValue" : 100, "rmg" : { + "zoneLimit" : 1, "value" : 100, "rarity" : 100 }, @@ -253,6 +254,7 @@ "index" : 0, "aiValue" : 100, "rmg" : { + "zoneLimit" : 1, "value" : 100, "rarity" : 20 }, From 65a8478b74c1d2236c64e1cb680e8ea803cff4a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Sat, 10 Jun 2023 15:07:03 +0200 Subject: [PATCH 11/16] Fix override --- lib/rmg/modificators/RoadPlacer.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rmg/modificators/RoadPlacer.h b/lib/rmg/modificators/RoadPlacer.h index f7772e361..531b0407d 100644 --- a/lib/rmg/modificators/RoadPlacer.h +++ b/lib/rmg/modificators/RoadPlacer.h @@ -19,7 +19,7 @@ public: MODIFICATOR(RoadPlacer); void process() override; - void init(); + void init() override; char dump(const int3 &) override; void addRoadNode(const int3 & node); From 983633d73b4c81dbd490476e1d247198e5d3b37a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Sat, 10 Jun 2023 18:02:26 +0200 Subject: [PATCH 12/16] Do not decrease the density of obstacles in zones of medium treasure value. For high values, decrease them more rapidly. --- lib/rmg/Zone.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/rmg/Zone.cpp b/lib/rmg/Zone.cpp index eb459d993..8da771c6d 100644 --- a/lib/rmg/Zone.cpp +++ b/lib/rmg/Zone.cpp @@ -215,10 +215,10 @@ void Zone::fractalize() treasureDensity += t.density; } - if (treasureValue > 100) + if (treasureValue > 200) { //Less obstacles - max span is 1 (no obstacles) - spanFactor = 1.0f - ((std::max(0, (1000 - treasureValue)) / 1000.f) * (1 - spanFactor)); + spanFactor = 1.0f - ((std::max(0, (1000 - treasureValue)) / (1000.f - 200)) * (1 - spanFactor)); } else if (treasureValue < 100) { From 3d3c97bf93e21839f09b3de068b5a4d00d4551a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Mon, 12 Jun 2023 20:32:58 +0200 Subject: [PATCH 13/16] Fix potential infinite loop --- lib/rmg/modificators/TreasurePlacer.cpp | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/rmg/modificators/TreasurePlacer.cpp b/lib/rmg/modificators/TreasurePlacer.cpp index 6ffed6e4c..6d77c88a8 100644 --- a/lib/rmg/modificators/TreasurePlacer.cpp +++ b/lib/rmg/modificators/TreasurePlacer.cpp @@ -763,17 +763,13 @@ void TreasurePlacer::createTreasures(ObjectManager& manager) //this is squared distance for optimization purposes const float minDistance = std::max((125.f / totalDensity), 1.0f); - for (size_t i = 0; i < count;) + for (size_t i = 0; i < count; i++) { auto treasurePileInfos = prepareTreasurePile(t); if (treasurePileInfos.empty()) { continue; } - else - { - i++; - } int value = std::accumulate(treasurePileInfos.begin(), treasurePileInfos.end(), 0, [](int v, const ObjectInfo* oi) {return v + oi->value; }); From 5219f1db9ddfa56c15718cb87188b018bff4ac7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Mon, 12 Jun 2023 20:33:21 +0200 Subject: [PATCH 14/16] Fix useless template setting --- Mods/vcmi/config/vcmi/rmg/hdmod/longRun.JSON | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/longRun.JSON b/Mods/vcmi/config/vcmi/rmg/hdmod/longRun.JSON index e4ca7f4ec..2e7cab25b 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmod/longRun.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmod/longRun.JSON @@ -233,9 +233,7 @@ "mines" : { "wood" : 0, "mercury" : 0, "ore" : 0, "sulfur" : 0, "crystal" : 0, "gems" : 0, "gold" : 10 }, "treasure" : [ - { "min" : 30000, "max" : 90000, "density" : 25 }, - { "min" : 0, "max" : 0, "density" : 1 }, - { "min" : 0, "max" : 0, "density" : 1 } + { "min" : 30000, "max" : 90000, "density" : 25 } ] }, "6" : From 6ebbdf0537af5d326d3e89182f06b49c603800e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Mon, 12 Jun 2023 20:51:04 +0200 Subject: [PATCH 15/16] More cleanup of Long Run template --- Mods/vcmi/config/vcmi/rmg/hdmod/longRun.JSON | 24 ++++++++------------ 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/longRun.JSON b/Mods/vcmi/config/vcmi/rmg/hdmod/longRun.JSON index 2e7cab25b..5adc35872 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmod/longRun.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmod/longRun.JSON @@ -18,8 +18,7 @@ "treasure" : [ { "min" : 300, "max" : 3000, "density" : 12 }, - { "min" : 5000, "max" : 9000, "density" : 6 }, - { "min" : 0, "max" : 0, "density" : 1 } + { "min" : 5000, "max" : 9000, "density" : 6 } ] }, "2" : @@ -34,8 +33,8 @@ "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, "treasure" : [ - { "min" : 5000, "max" : 7000, "density" : 30 }, { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 5000, "max" : 7000, "density" : 30 }, { "min" : 300, "max" : 3000, "density" : 5 } ] }, @@ -51,8 +50,8 @@ "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 0 }, "treasure" : [ - { "min" : 12000, "max" : 16000, "density" : 5 }, { "min" : 20000, "max" : 21000, "density" : 6 }, + { "min" : 12000, "max" : 16000, "density" : 5 }, { "min" : 300, "max" : 3000, "density" : 5 } ] }, @@ -69,8 +68,7 @@ "treasure" : [ { "min" : 25000, "max" : 30000, "density" : 10 }, - { "min" : 300, "max" : 3000, "density" : 10 }, - { "min" : 0, "max" : 0, "density" : 1 } + { "min" : 300, "max" : 3000, "density" : 10 } ] }, "5" : @@ -85,9 +83,7 @@ "mines" : { "wood" : 0, "mercury" : 0, "ore" : 0, "sulfur" : 0, "crystal" : 0, "gems" : 0, "gold" : 4 }, "treasure" : [ - { "min" : 30000, "max" : 90000, "density" : 25 }, - { "min" : 0, "max" : 0, "density" : 1 }, - { "min" : 0, "max" : 0, "density" : 1 } + { "min" : 30000, "max" : 90000, "density" : 25 } ] }, "6" : @@ -167,8 +163,7 @@ "treasure" : [ { "min" : 300, "max" : 3000, "density" : 12 }, - { "min" : 5000, "max" : 9000, "density" : 6 }, - { "min" : 0, "max" : 0, "density" : 1 } + { "min" : 5000, "max" : 9000, "density" : 6 } ] }, "2" : @@ -183,8 +178,8 @@ "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, "treasure" : [ - { "min" : 5000, "max" : 7000, "density" : 10 }, { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 5000, "max" : 7000, "density" : 10 }, { "min" : 300, "max" : 3000, "density" : 5 } ] }, @@ -200,8 +195,8 @@ "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 0 }, "treasure" : [ - { "min" : 12000, "max" : 16000, "density" : 5 }, { "min" : 20000, "max" : 21000, "density" : 6 }, + { "min" : 12000, "max" : 16000, "density" : 5 }, { "min" : 300, "max" : 3000, "density" : 5 } ] }, @@ -218,8 +213,7 @@ "treasure" : [ { "min" : 25000, "max" : 30000, "density" : 10 }, - { "min" : 300, "max" : 3000, "density" : 10 }, - { "min" : 0, "max" : 0, "density" : 1 } + { "min" : 300, "max" : 3000, "density" : 10 } ] }, "5" : From e1f8ae94ac89881e9214a2bbbd1c8ec49764f846 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Mon, 12 Jun 2023 22:15:59 +0200 Subject: [PATCH 16/16] + Do not limit treasures to make space for more treasures if there are none ;eft + Make sure RMG won't get stuck in infinite loop for weird treasure values (eg. 0). --- lib/rmg/modificators/TreasurePlacer.cpp | 26 ++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/lib/rmg/modificators/TreasurePlacer.cpp b/lib/rmg/modificators/TreasurePlacer.cpp index 6d77c88a8..0e6b3c9d8 100644 --- a/lib/rmg/modificators/TreasurePlacer.cpp +++ b/lib/rmg/modificators/TreasurePlacer.cpp @@ -741,33 +741,41 @@ void TreasurePlacer::createTreasures(ObjectManager& manager) int totalDensity = 0; - for (auto t : treasureInfo) + for (auto t = treasureInfo.begin(); t != treasureInfo.end(); t++) { std::vector treasures; //discard objects with too high value to be ever placed vstd::erase_if(possibleObjects, [t](const ObjectInfo& oi) -> bool { - return oi.value > t.max; + return oi.value > t->max; }); - totalDensity += t.density; + totalDensity += t->density; - size_t count = size * t.density / 500; - const int averageValue = (t.min + t.max) / 2; - if (averageValue > 10000) //Will surely be guarded => larger + size_t count = size * t->density / 500; + + //Assure space for lesser treasures, if there are any left + if (t != (treasureInfo.end() - 1)) { - vstd::amin(count, size * (10.f/500) / (std::sqrt((float)averageValue / 10000))); + const int averageValue = (t->min + t->max) / 2; + if (averageValue > 10000) + { + //Will surely be guarded => larger piles => less space inbetween + vstd::amin(count, size * (10.f / 500) / (std::sqrt((float)averageValue / 10000))); + } } //this is squared distance for optimization purposes const float minDistance = std::max((125.f / totalDensity), 1.0f); - for (size_t i = 0; i < count; i++) + size_t emergencyLoopFinish = 0; + while(treasures.size() < count && emergencyLoopFinish < count) { - auto treasurePileInfos = prepareTreasurePile(t); + auto treasurePileInfos = prepareTreasurePile(*t); if (treasurePileInfos.empty()) { + emergencyLoopFinish++; //Exit potentially infinite loop for bad settings continue; }