add requiredObjects to customObjects rmg zone configuration

- add requiredObjects to customObjects
- add requiredObjects functionality to template editor
- add capability to specify guard info
- add documentation
- extend template schema
- extend serialization
This commit is contained in:
Andrej Dudenhefner
2026-02-01 12:24:34 +01:00
parent 09b144d383
commit 93f583bd8c
14 changed files with 380 additions and 8 deletions
+33
View File
@@ -106,6 +106,39 @@
}
},
"requiredObjects": {
"type": "object",
"additionalProperties": {
"anyOf" : [
{ "type": "number", "minimum" : 0 },
{
"type": "object",
"additionalProperties": { "type": "number", "minimum" : 0 }
},
{
"type": "object",
"properties": {
"count": { "type": "number", "minimum": 0 },
"guard": { "type": "number", "minimum": 0 }
},
"required": ["count"],
"additionalProperties": false
},
{
"type": "object",
"additionalProperties": {
"type": "object",
"properties": {
"count": { "type": "number", "minimum": 0 },
"guard": { "type": "number", "minimum": 0 }
},
"required": ["count"],
"additionalProperties": false
}
}
]
}
},
"commonObjects": {
"type": "array",
"items": {
+15 -2
View File
@@ -172,7 +172,7 @@
// Mines will have same configuration as in linked zone
"minesLikeZone" : 1,
// Treasures will have same configuration as in linked zone
"treasureLikeZone" : 1,
@@ -276,7 +276,20 @@
"zoneLimit" : 2
}
}
]
],
// Required objects that will be added to this zone guarded according to rmg.value of the individual objects
"requiredObjects" : {
"refugeeCamp" : 5,
// If there are several versions (e.g. standard and cannonYard) a random selection is chosen
"warMachineFactory" : 5,
// Versions can be specified with optional guard information
"creatureGeneratorCommon" : { "unicornGlade" : { "count" : 3, "guard" : 10000 } },
// Artifacts can be specified
"artifact" : { "headOfLegion" : 1, "armsOfLegion" : 1, "torsoOfLegion" : 1, "loinsOfLegion" : 1, "legsOfLegion" : 1 },
// Mod objects can be specified
"hota.mapobjects:townGate" : { "count" : 1, "guard" : 100000 }
},
}
}
```
+2
View File
@@ -237,6 +237,7 @@ set(lib_MAIN_SRCS
rmg/modificators/ConnectionsPlacer.cpp
rmg/modificators/WaterAdopter.cpp
rmg/modificators/MinePlacer.cpp
rmg/modificators/ObjectPlacer.cpp
rmg/modificators/TownPlacer.cpp
rmg/modificators/WaterProxy.cpp
rmg/modificators/WaterRoutes.cpp
@@ -702,6 +703,7 @@ set(lib_MAIN_HEADERS
rmg/modificators/ConnectionsPlacer.h
rmg/modificators/WaterAdopter.h
rmg/modificators/MinePlacer.h
rmg/modificators/ObjectPlacer.h
rmg/modificators/TownPlacer.h
rmg/modificators/WaterProxy.h
rmg/modificators/WaterRoutes.h
+3 -3
View File
@@ -1005,13 +1005,13 @@ void CRmgTemplate::afterLoad()
&rmg::ZoneOptions::setTerrainTypes,
&rmg::ZoneOptions::getTerrainTypeLikeZone,
"terrain types");
inheritZoneProperty(zone,
&rmg::ZoneOptions::getMinesInfo,
&rmg::ZoneOptions::setMinesInfo,
&rmg::ZoneOptions::getMinesLikeZone,
"mine types");
inheritZoneProperty(zone,
&rmg::ZoneOptions::getTreasureInfo,
&rmg::ZoneOptions::setTreasureInfo,
@@ -1043,7 +1043,7 @@ void CRmgTemplate::afterLoad()
zone1->addConnection(connection);
zone2->addConnection(connection);
}
if(allowedWaterContent.empty() || allowedWaterContent.count(EWaterContent::RANDOM))
{
allowedWaterContent.insert(EWaterContent::NONE);
+100
View File
@@ -43,6 +43,25 @@ void ObjectConfig::addCustomObject(const ObjectInfo & object)
logGlobal->info("Added custom object of type %d.%d", object.primaryID, object.secondaryID);
}
void ObjectConfig::addRequiredObject(const CompoundMapObjectID & objid, ui16 count, std::optional<ui32> guardLevel)
{
requiredObjects[objid] = std::pair{count, guardLevel};
logGlobal->info("Added required object of type %d.%d, count: %d, guard level: %d",
objid.primaryID, objid.secondaryID, count,
guardLevel.has_value() ? std::to_string(guardLevel.value()) : "none");
}
void ObjectConfig::addRequiredObject(const CompoundMapObjectID & objid, std::pair<ui16, std::optional<ui32>> info)
{
addRequiredObject(objid, info.first, info.second);
}
void ObjectConfig::clearRequiredObjects()
{
requiredObjects.clear();
}
void ObjectConfig::serializeJson(JsonSerializeFormat & handler)
{
static const std::map<std::string, EObjectCategory> OBJECT_CATEGORY_STRINGS = {
@@ -271,6 +290,82 @@ void ObjectConfig::serializeJson(JsonSerializeFormat & handler)
}
}
}
// Serialize required objects
if(handler.saving)
{
if(!requiredObjects.empty())
{
auto requiredObjectsStruct = handler.enterStruct("requiredObjects");
for(const auto & [objId, info] : requiredObjects)
{
std::string jsonKey = LIBRARY->objtypeh->getJsonKey(objId.primaryID);
auto entryStruct = requiredObjectsStruct->enterStruct(jsonKey);
auto objHandler = LIBRARY->objtypeh->getHandlerFor(objId);
std::string jsonSubKey = objHandler->getJsonKey();
auto subEntryStruct = requiredObjectsStruct->enterStruct(jsonSubKey);
ui16 countVal = info.first;
subEntryStruct->serializeInt("count", countVal);
if (info.second.has_value())
{
ui32 guardVal = info.second.value();
subEntryStruct->serializeInt("guard", guardVal);
}
}
}
}
else
{
const JsonNode & config = handler.getCurrent();
const JsonNode & configRequiredObjects = config["requiredObjects"];
const auto getInfo = [](const JsonNode & node) {
auto & child = node.Struct();
ui16 countVal = static_cast<ui16>(child.at("count").Integer());
std::optional<ui32> guardVal = std::nullopt;
if (child.contains("guard"))
guardVal = static_cast<ui32>(child.at("guard").Integer());
return std::pair{countVal, guardVal};
};
for(const auto & node : configRequiredObjects.Struct())
{
LIBRARY->identifiers()->requestIdentifierIfFound(node.second.getModScope(), "object", node.first, [this, node, &getInfo](int primaryID)
{
if (node.second.isNumber())
{
addRequiredObject(CompoundMapObjectID(primaryID, -1), static_cast<ui16>(node.second.Integer()));
}
else
{
if (node.second.Struct().contains("count"))
{
addRequiredObject(CompoundMapObjectID(primaryID, -1), getInfo(node.second));
}
else
{
// secondary ID specified
for (const auto & subNode : node.second.Struct())
{
std::string jsonKey = LIBRARY->objtypeh->getJsonKey(primaryID);
if(node.first == "artifact" || node.first == "core:artifact")
//FIXME: core:artifact is not resolved properly by requestIdentifierIfFound
jsonKey = "artifact";
LIBRARY->identifiers()->requestIdentifierIfFound(node.second.getModScope(), jsonKey, subNode.first, [this, primaryID, subNode, &getInfo](int secondaryID)
{
if(subNode.second.isNumber())
addRequiredObject(CompoundMapObjectID(primaryID, secondaryID), static_cast<ui16>(subNode.second.Integer()));
else
addRequiredObject(CompoundMapObjectID(primaryID, secondaryID), getInfo(subNode.second));
});
}
}
}
});
}
}
}
const std::vector<ObjectInfo> & ObjectConfig::getConfiguredObjects() const
@@ -288,4 +383,9 @@ const std::vector<ObjectConfig::EObjectCategory> & ObjectConfig::getBannedObject
return bannedObjectCategories;
}
std::map<CompoundMapObjectID, std::pair<ui16, std::optional<ui32>>> ObjectConfig::getRequiredObjects() const
{
return requiredObjects;
}
VCMI_LIB_NAMESPACE_END
+7
View File
@@ -41,12 +41,17 @@ public:
void addBannedObject(const CompoundMapObjectID & objid);
void addCustomObject(const ObjectInfo & object);
void addRequiredObject(const CompoundMapObjectID & objid, ui16 count, std::optional<ui32> guardLevel = std::nullopt);
void addRequiredObject(const CompoundMapObjectID & objid, std::pair<ui16, std::optional<ui32>> info);
void clearBannedObjects();
void clearCustomObjects();
void clearRequiredObjects();
const std::vector<CompoundMapObjectID> & getBannedObjects() const;
const std::vector<EObjectCategory> & getBannedObjectCategories() const;
const std::vector<ObjectInfo> & getConfiguredObjects() const;
std::map<CompoundMapObjectID, std::pair<ui16, std::optional<ui32>>> getRequiredObjects() const;
void serializeJson(JsonSerializeFormat & handler);
private:
// TODO: Add convenience method for banning objects by name
@@ -56,6 +61,8 @@ private:
// TODO: In what format should I store custom objects?
// Need to convert map serialization format to ObjectInfo
std::vector<ObjectInfo> customObjects;
std::map<CompoundMapObjectID, std::pair<ui16, std::optional<ui32>>> requiredObjects; // obligatory, potentially guarded objects to spawn in this zone
};
VCMI_LIB_NAMESPACE_END
+2
View File
@@ -25,6 +25,7 @@
#include "modificators/ConnectionsPlacer.h"
#include "modificators/TownPlacer.h"
#include "modificators/MinePlacer.h"
#include "modificators/ObjectPlacer.h"
#include "modificators/ObjectDistributor.h"
#include "modificators/WaterAdopter.h"
#include "modificators/WaterProxy.h"
@@ -165,6 +166,7 @@ void RmgMap::addModificators()
{
zone->addModificator<TownPlacer>();
zone->addModificator<MinePlacer>();
zone->addModificator<ObjectPlacer>();
zone->addModificator<QuestArtifactPlacer>();
zone->addModificator<ConnectionsPlacer>();
zone->addModificator<RoadPlacer>();
+2
View File
@@ -19,6 +19,7 @@
#include "ConnectionsPlacer.h"
#include "TownPlacer.h"
#include "MinePlacer.h"
#include "ObjectPlacer.h"
#include "QuestArtifactPlacer.h"
#include "../../CCreatureHandler.h"
#include "../../mapObjectConstructors/AObjectTypeHandler.h"
@@ -69,6 +70,7 @@ void ObjectManager::init()
DEPENDENCY(TownPlacer); //Only secondary towns
DEPENDENCY(MinePlacer);
DEPENDENCY(ObjectPlacer);
POSTFUNCTION(RoadPlacer);
createDistancesPriorityQueue();
}
+82
View File
@@ -0,0 +1,82 @@
/*
* License: GNU General Public License v2.0 or later
* Full text of license available in license.txt file, in main folder
*
*/
#include "StdInc.h"
#include "ObjectPlacer.h"
#include "TownPlacer.h"
#include "ConnectionsPlacer.h"
#include "../../mapObjectConstructors/AObjectTypeHandler.h"
#include "ObjectManager.h"
#include "RoadPlacer.h"
#include <vstd/RNG.h>
VCMI_LIB_NAMESPACE_BEGIN
void ObjectPlacer::process()
{
auto * manager = zone.getModificator<ObjectManager>();
if(!manager)
{
logGlobal->error("ObjectManager doesn't exist for zone %d, skip modificator %s", zone.getId(), getName());
return;
}
placeRequiredObjects(*manager);
}
void ObjectPlacer::init()
{
DEPENDENCY(TownPlacer);
DEPENDENCY(ConnectionsPlacer);
POSTFUNCTION(ObjectManager);
POSTFUNCTION(RoadPlacer);
}
bool ObjectPlacer::placeRequiredObjects(ObjectManager & manager)
{
std::vector<std::pair<std::shared_ptr<CGObjectInstance>, ui32>> requiredObjects;
for(const auto & [objid, info] : zone.getCustomObjects().getRequiredObjects())
{
for(int i = 0; i < info.first; ++i)
{
auto secondaryID = objid.secondaryID;
// Unspecified subtype - pick random subobject
if(secondaryID == -1)
{
auto subObjects = LIBRARY->objtypeh->knownSubObjects(objid.primaryID);
if(subObjects.empty())
{
logGlobal->error("No subobjects for object type %d", objid.primaryID);
continue;
}
secondaryID = *RandomGeneratorUtil::nextItem(subObjects, zone.getRand());
}
TObjectTypeHandler handler = LIBRARY->objtypeh->getHandlerFor(objid.primaryID, secondaryID);
assert(handler);
ui32 guardStrength = info.second.has_value() ? info.second.value() : handler->getRMGInfo().value;
auto object = handler->create(map.mapInstance->cb, nullptr);
if (object->asOwnable() && object->tempOwner == PlayerColor::UNFLAGGABLE)
object->setOwner(PlayerColor::NEUTRAL);
requiredObjects.emplace_back(object, guardStrength);
}
}
//Shuffle objects to avoid patterns, but don't shuffle key objects like towns
RandomGeneratorUtil::randomShuffle(requiredObjects, zone.getRand());
for (const auto& obj : requiredObjects)
{
manager.addRequiredObject(RequiredObjectInfo(obj.first, obj.second));
}
return true;
}
VCMI_LIB_NAMESPACE_END
+29
View File
@@ -0,0 +1,29 @@
/*
* ObjectPlacer.h, part of VCMI engine
*
* Authors: listed in file AUTHORS in main folder
*
* License: GNU General Public License v2.0 or later
* Full text of license available in license.txt file, in main folder
*
*/
#pragma once
#include "../Zone.h"
VCMI_LIB_NAMESPACE_BEGIN
class ObjectManager;
class ObjectPlacer: public Modificator
{
public:
MODIFICATOR(ObjectPlacer);
void process() override;
void init() override;
protected:
bool placeRequiredObjects(ObjectManager & manager);
};
VCMI_LIB_NAMESPACE_END
+2
View File
@@ -25,6 +25,7 @@
#include "../Functions.h"
#include "RoadPlacer.h"
#include "MinePlacer.h"
#include "ObjectPlacer.h"
#include "WaterAdopter.h"
#include "../TileInfo.h"
@@ -69,6 +70,7 @@ void TownPlacer::init()
}
}
POSTFUNCTION(MinePlacer);
POSTFUNCTION(ObjectPlacer);
POSTFUNCTION(RoadPlacer);
}
+90 -2
View File
@@ -37,6 +37,7 @@ ObjectSelector::ObjectSelector(ObjectConfig & obj) :
fillBannedObjectCategories();
fillBannedObjects();
fillCustomObjects();
fillRequiredObjects();
show();
}
@@ -216,6 +217,92 @@ void ObjectSelector::getBannedObjects()
}
}
void ObjectSelector::fillRequiredObjects()
{
auto reqObjs = obj.getRequiredObjects();
ui->tableWidgetRequiredObjects->setColumnCount(4);
ui->tableWidgetRequiredObjects->setRowCount(reqObjs.size() + 1);
ui->tableWidgetRequiredObjects->setHorizontalHeaderLabels({tr("Object"), tr("Count"), tr("Guard"), tr("Action")});
auto addRow = [this](CompoundMapObjectID objId, ui16 count, std::optional<ui32> guard, int row){
// Column 0: Object selector (QComboBox with searchable dropdown)
QComboBox *combo = new QComboBox();
for(auto & item : advObjects)
combo->addItem(item.second, QVariant::fromValue(item.first));
int index = combo->findData(QVariant::fromValue(objId));
if (index != -1)
combo->setCurrentIndex(index);
combo->setEditable(true);
QCompleter* completer = new QCompleter(combo);
completer->setModel(combo->model());
completer->setCompletionMode(QCompleter::PopupCompletion);
completer->setFilterMode(Qt::MatchContains);
combo->setCompleter(completer);
ui->tableWidgetRequiredObjects->setCellWidget(row, 0, combo);
// Column 1: Count (QSpinBox, range 1-100)
QSpinBox *spinCount = new QSpinBox();
spinCount->setRange(1, 100);
spinCount->setValue(count);
ui->tableWidgetRequiredObjects->setCellWidget(row, 1, spinCount);
// Column 2: Guard level (QSpinBox, range -1-999999, -1 = no guard)
QSpinBox *spinGuard = new QSpinBox();
spinGuard->setRange(-1, 999999);
spinGuard->setValue(guard.value_or(-1));
spinGuard->setSpecialValueText(tr("None")); // Shows "None" when value is -1
ui->tableWidgetRequiredObjects->setCellWidget(row, 2, spinGuard);
// Column 3: Delete button
auto deleteButton = new QPushButton(tr("Delete"));
ui->tableWidgetRequiredObjects->setCellWidget(row, 3, deleteButton);
connect(deleteButton, &QPushButton::clicked, this, [this, deleteButton]() {
for (int r = 0; r < ui->tableWidgetRequiredObjects->rowCount(); ++r) {
if (ui->tableWidgetRequiredObjects->cellWidget(r, 3) == deleteButton) {
ui->tableWidgetRequiredObjects->removeRow(r);
break;
}
}
});
};
int row = 0;
for (const auto & [objId, info] : reqObjs)
addRow(objId, info.first, info.second, row++);
// Add "Add" button in last row
auto addButton = new QPushButton(tr("Add"));
ui->tableWidgetRequiredObjects->setCellWidget(ui->tableWidgetRequiredObjects->rowCount() - 1, 3, addButton);
connect(addButton, &QPushButton::clicked, this, [this, addRow]() {
ui->tableWidgetRequiredObjects->insertRow(ui->tableWidgetRequiredObjects->rowCount() - 1);
addRow((*advObjects.begin()).first, 1, std::nullopt, ui->tableWidgetRequiredObjects->rowCount() - 2);
});
ui->tableWidgetRequiredObjects->resizeColumnsToContents();
ui->tableWidgetRequiredObjects->setColumnWidth(0, 400);
}
void ObjectSelector::getRequiredObjects()
{
obj.clearRequiredObjects();
for (int row = 0; row < ui->tableWidgetRequiredObjects->rowCount() - 1; ++row)
{
auto id = static_cast<QComboBox *>(ui->tableWidgetRequiredObjects->cellWidget(row, 0))->currentData().value<CompoundMapObjectID>();
auto count = static_cast<QSpinBox *>(ui->tableWidgetRequiredObjects->cellWidget(row, 1))->value();
auto guardValue = static_cast<QSpinBox *>(ui->tableWidgetRequiredObjects->cellWidget(row, 2))->value();
std::optional<ui32> guard = std::nullopt;
if (guardValue >= 0)
guard = guardValue;
obj.addRequiredObject(id, count, guard);
}
}
void ObjectSelector::fillCustomObjects()
{
ui->tableWidgetObjects->setColumnCount(5);
@@ -310,11 +397,12 @@ void ObjectSelector::on_buttonBoxResult_accepted()
getBannedObjectCategories();
getBannedObjects();
getCustomObjects();
getRequiredObjects();
close();
close();
}
void ObjectSelector::on_buttonBoxResult_rejected()
{
close();
close();
}
@@ -45,4 +45,6 @@ private:
void getBannedObjects();
void fillCustomObjects();
void getCustomObjects();
void fillRequiredObjects();
void getRequiredObjects();
};
+11 -1
View File
@@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>750</width>
<height>480</height>
<height>600</height>
</rect>
</property>
<property name="windowTitle">
@@ -24,6 +24,16 @@
<item>
<widget class="QTableWidget" name="tableWidgetObjects"/>
</item>
<item>
<widget class="QLabel" name="labelRequiredObjects">
<property name="text">
<string>Required Objects</string>
</property>
</widget>
</item>
<item>
<widget class="QTableWidget" name="tableWidgetRequiredObjects"/>
</item>
<item>
<widget class="QLabel" name="labelBannedObjects">
<property name="text">