Веб-сайт самохостера Lotigara

summaryrefslogtreecommitdiff
path: root/source/game/StarDungeonGenerator.cpp
diff options
context:
space:
mode:
authorKae <80987908+Novaenia@users.noreply.github.com>2023-06-20 14:33:09 +1000
committerKae <80987908+Novaenia@users.noreply.github.com>2023-06-20 14:33:09 +1000
commit6352e8e3196f78388b6c771073f9e03eaa612673 (patch)
treee23772f79a7fbc41bc9108951e9e136857484bf4 /source/game/StarDungeonGenerator.cpp
parent6741a057e5639280d85d0f88ba26f000baa58f61 (diff)
everything everywhere
all at once
Diffstat (limited to 'source/game/StarDungeonGenerator.cpp')
-rw-r--r--source/game/StarDungeonGenerator.cpp1602
1 files changed, 1602 insertions, 0 deletions
diff --git a/source/game/StarDungeonGenerator.cpp b/source/game/StarDungeonGenerator.cpp
new file mode 100644
index 0000000..b12c1e1
--- /dev/null
+++ b/source/game/StarDungeonGenerator.cpp
@@ -0,0 +1,1602 @@
+#include "StarDungeonGenerator.hpp"
+#include "StarCasting.hpp"
+#include "StarRandom.hpp"
+#include "StarLogging.hpp"
+#include "StarAssets.hpp"
+#include "StarLexicalCast.hpp"
+#include "StarJsonExtra.hpp"
+#include "StarMaterialDatabase.hpp"
+#include "StarRoot.hpp"
+#include "StarLiquidsDatabase.hpp"
+#include "StarDungeonImagePart.hpp"
+#include "StarDungeonTMXPart.hpp"
+
+namespace Star {
+
+size_t const DefinitionsCacheSize = 20;
+
+namespace Dungeon {
+
+ EnumMap<Dungeon::Direction> const DungeonDirectionNames{
+ {Dungeon::Direction::Left, "left"},
+ {Dungeon::Direction::Right, "right"},
+ {Dungeon::Direction::Up, "up"},
+ {Dungeon::Direction::Down, "down"},
+ {Dungeon::Direction::Unknown, "unknown"},
+ {Dungeon::Direction::Any, "any"},
+ };
+
+ Direction flipDirection(Direction direction) {
+ if (direction == Direction::Left)
+ return Direction::Right;
+ if (direction == Direction::Right)
+ return Direction::Left;
+ if (direction == Direction::Up)
+ return Direction::Down;
+ if (direction == Direction::Down)
+ return Direction::Up;
+ if (direction == Direction::Any)
+ return Direction::Any;
+ throw DungeonException("Invalid direction");
+ }
+
+ MaterialId biomeMaterialForJson(int variant) {
+ if (variant == 0)
+ return BiomeMaterialId;
+ if (variant == 1)
+ return Biome1MaterialId;
+ if (variant == 2)
+ return Biome2MaterialId;
+ if (variant == 3)
+ return Biome3MaterialId;
+ if (variant == 4)
+ return Biome4MaterialId;
+ starAssert(variant == 5);
+ return Biome5MaterialId;
+ }
+
+ ConnectorConstPtr chooseOption(List<ConnectorConstPtr>& options, RandomSource& rnd) {
+ float distribution = 0;
+ for (size_t i = 0; i < options.size(); i++)
+ distribution += options[i]->part()->chance();
+ float pick = rnd.randf() * distribution;
+ for (size_t i = 0; i < options.size(); i++) {
+ pick -= options[i]->part()->chance();
+ if (pick <= 0)
+ return options.takeAt(i);
+ }
+ // float rounding is always fun
+ return options.takeAt(options.size() - 1);
+ }
+
+ List<RuleConstPtr> Rule::readRules(Json const& rules) {
+ List<RuleConstPtr> result;
+ for (auto const& list : rules.iterateArray()) {
+ Maybe<RuleConstPtr> rule = Rule::parse(list);
+ if (rule.isValid())
+ result.push_back(*rule);
+ }
+ return result;
+ }
+
+ List<BrushConstPtr> Brush::readBrushes(Json const& brushes) {
+ List<BrushConstPtr> result;
+ for (auto const& list : brushes.iterateArray())
+ result.push_back(Brush::parse(list));
+ return result;
+ }
+
+ Maybe<RuleConstPtr> Rule::parse(Json const& rule) {
+ String key = rule.getString(0);
+ if (key == "worldGenMustContainLiquid")
+ return as<Rule>(make_shared<const WorldGenMustContainLiquidRule>());
+ if (key == "worldGenMustNotContainLiquid")
+ return as<Rule>(make_shared<const WorldGenMustNotContainLiquidRule>());
+
+ if (key == "worldGenMustContainSolidForeground")
+ return as<Rule>(make_shared<const WorldGenMustContainSolidRule>(TileLayer::Foreground));
+ if (key == "worldGenMustContainAirForeground")
+ return as<Rule>(make_shared<const WorldGenMustContainAirRule>(TileLayer::Foreground));
+
+ if (key == "worldGenMustContainSolidBackground")
+ return as<Rule>(make_shared<const WorldGenMustContainSolidRule>(TileLayer::Background));
+ if (key == "worldGenMustContainAirBackground")
+ return as<Rule>(make_shared<const WorldGenMustContainAirRule>(TileLayer::Background));
+
+ if (key == "allowOverdrawing")
+ return as<Rule>(make_shared<const AllowOverdrawingRule>());
+ if (key == "ignorePartMaximumRule")
+ return as<Rule>(make_shared<const IgnorePartMaximumRule>());
+ if (key == "maxSpawnCount")
+ return as<Rule>(make_shared<const MaxSpawnCountRule>(rule));
+ if (key == "doNotConnectToPart")
+ return as<Rule>(make_shared<const DoNotConnectToPartRule>(rule));
+ if (key == "doNotCombineWith")
+ return as<Rule>(make_shared<const DoNotCombineWithRule>(rule));
+
+ Logger::error("Unknown dungeon rule: %s", key);
+ return Maybe<RuleConstPtr>();
+ }
+
+ bool Rule::checkTileCanPlace(Vec2I, DungeonGeneratorWriter*) const {
+ return true;
+ }
+
+ bool Rule::overdrawable() const {
+ return false;
+ }
+
+ bool Rule::ignorePartMaximum() const {
+ return false;
+ }
+
+ bool Rule::allowSpawnCount(int) const {
+ return true;
+ }
+
+ bool Rule::doesNotConnectToPart(String const&) const {
+ return false;
+ }
+
+ bool Rule::checkPartCombinationsAllowed(StringMap<int> const&) const {
+ return true;
+ }
+
+ bool Rule::requiresOpen() const {
+ return false;
+ }
+
+ bool Rule::requiresSolid() const {
+ return false;
+ }
+
+ bool Rule::requiresLiquid() const {
+ return false;
+ }
+
+ BrushConstPtr parseFrontBrush(Json const& brush) {
+ String material;
+ Maybe<String> mod;
+ Maybe<float> hueshift, modhueshift;
+ Maybe<MaterialColorVariant> colorVariant;
+
+ if (brush.isType(Json::Type::Object)) {
+ material = brush.getString("material");
+ mod = brush.optString("mod");
+ hueshift = brush.optFloat("hueshift");
+ modhueshift = brush.optFloat("modhueshift");
+ colorVariant = brush.optFloat("colorVariant");
+ } else {
+ material = brush.getString(1);
+ if (brush.size() > 2)
+ mod = brush.getString(2);
+ }
+ return make_shared<const FrontBrush>(material, mod, hueshift, modhueshift, colorVariant);
+ }
+
+ BrushConstPtr parseBackBrush(Json const& brush) {
+ String material;
+ Maybe<String> mod;
+ Maybe<float> hueshift, modhueshift;
+ Maybe<MaterialColorVariant> colorVariant;
+
+ if (brush.isType(Json::Type::Object)) {
+ material = brush.getString("material");
+ mod = brush.optString("mod");
+ hueshift = brush.optFloat("hueshift");
+ modhueshift = brush.optFloat("modhueshift");
+ colorVariant = brush.optFloat("colorVariant");
+ } else {
+ material = brush.getString(1);
+ if (brush.size() > 2)
+ mod = brush.getString(2);
+ }
+ return make_shared<const BackBrush>(material, mod, hueshift, modhueshift, colorVariant);
+ }
+
+ BrushConstPtr parseObjectBrush(Json const& brush) {
+ String object;
+ Star::Direction direction;
+ Json parameters;
+
+ object = brush.getString(1);
+ JsonObject settings;
+ if (brush.size() > 2)
+ settings = brush.getObject(2);
+ if (settings.contains("direction"))
+ direction = DirectionNames.getLeft(settings.get("direction").toString());
+ else
+ direction = Star::Direction::Left;
+
+ if (settings.contains("parameters"))
+ parameters = settings.get("parameters");
+ return make_shared<const ObjectBrush>(object, direction, parameters);
+ }
+
+ BrushConstPtr parseSurfaceBrush(Json const& brush) {
+ Json settings = Json::ofType(Json::Type::Object);
+ if (brush.size() > 1)
+ settings = brush.get(1);
+ return make_shared<const SurfaceBrush>(settings.optInt("variant"), settings.optString("mod"));
+ }
+
+ BrushConstPtr parseSurfaceBackgroundBrush(Json const& brush) {
+ Json settings = Json::ofType(Json::Type::Object);
+ if (brush.size() > 1)
+ settings = brush.get(1);
+ return make_shared<const SurfaceBackgroundBrush>(settings.optInt("variant"), settings.optString("mod"));
+ }
+
+ BrushConstPtr parseWireBrush(Json const& brush) {
+ Json settings = brush.get(1);
+ String group = settings.getString("group");
+ bool local = settings.getBool("local", true);
+ return make_shared<const WireBrush>(group, local);
+ }
+
+ BrushConstPtr parseItemBrush(Json const& brush) {
+ ItemDescriptor item(brush.getString(1), 1);
+ return make_shared<const ItemBrush>(item);
+ }
+
+ BrushConstPtr Brush::parse(Json const& brush) {
+ String key = brush.getString(0);
+ if (key == "clear")
+ return as<const Brush>(make_shared<ClearBrush>());
+
+ if (key == "front")
+ return parseFrontBrush(brush);
+ if (key == "back")
+ return parseBackBrush(brush);
+ if (key == "object")
+ return parseObjectBrush(brush);
+ if (key == "biomeitems")
+ return as<Brush>(make_shared<BiomeItemsBrush>());
+ if (key == "biometree")
+ return as<Brush>(make_shared<BiomeTreeBrush>());
+ if (key == "item")
+ return parseItemBrush(brush);
+ if (key == "npc")
+ return as<Brush>(make_shared<NpcBrush>(brush.get(1)));
+ if (key == "stagehand")
+ return as<Brush>(make_shared<StagehandBrush>(brush.get(1)));
+ if (key == "random")
+ return as<Brush>(make_shared<RandomBrush>(brush));
+ if (key == "surface")
+ return parseSurfaceBrush(brush);
+ if (key == "surfacebackground")
+ return parseSurfaceBackgroundBrush(brush);
+ if (key == "liquid")
+ return as<Brush>(make_shared<LiquidBrush>(brush.getString(1), 1.0f, brush.getBool(2, false)));
+ if (key == "wire")
+ return parseWireBrush(brush);
+ if (key == "playerstart")
+ return as<Brush>(make_shared<PlayerStartBrush>());
+ throw DungeonException::format("Unknown dungeon brush: %s", key);
+ }
+
+ RandomBrush::RandomBrush(Json const& brush) {
+ JsonArray options = brush.getArray(1);
+ for (auto const& option : options)
+ m_brushes.append(Brush::parse(option));
+ m_seed = Random::randi64();
+ }
+
+ void RandomBrush::paint(Vec2I position, Phase phase, DungeonGeneratorWriter* writer) const {
+ size_t rnd = (size_t)staticRandomI32(m_seed, position[0], position[1]);
+ m_brushes[rnd % m_brushes.size()]->paint(position, phase, writer);
+ }
+
+ void ClearBrush::paint(Vec2I position, Phase phase, DungeonGeneratorWriter* writer) const {
+ if (phase != Phase::ClearPhase)
+ return;
+
+ // TODO: delete objects too?
+ writer->setLiquid(position, LiquidStore(EmptyLiquidId, 0.0f, 0.0f, false));
+ writer->setForegroundMaterial(position, EmptyMaterialId, 0, DefaultMaterialColorVariant);
+ writer->setBackgroundMaterial(position, EmptyMaterialId, 0, DefaultMaterialColorVariant);
+ writer->setForegroundMod(position, NoModId, 0);
+ writer->setBackgroundMod(position, NoModId, 0);
+ }
+
+ FrontBrush::FrontBrush(String const& material, Maybe<String> mod, Maybe<float> hueshift, Maybe<float> modhueshift, Maybe<MaterialColorVariant> colorVariant) {
+ m_material = material;
+ m_mod = mod;
+ m_materialHue = hueshift.apply(materialHueFromDegrees).value(0);
+ m_modHue = modhueshift.apply(materialHueFromDegrees).value(0);
+ m_materialColorVariant = colorVariant.value(DefaultMaterialColorVariant);
+ }
+
+ void FrontBrush::paint(Vec2I position, Phase phase, DungeonGeneratorWriter* writer) const {
+ if (phase != Phase::WallPhase)
+ return;
+
+ auto materialDatabase = Root::singleton().materialDatabase();
+ MaterialId material = materialDatabase->materialId(m_material);
+
+ ModId mod = NoModId;
+ if (m_mod)
+ mod = materialDatabase->modId(*m_mod);
+
+ if (isSolidColliding(materialDatabase->materialCollisionKind(material)))
+ writer->setLiquid(position, LiquidStore(EmptyLiquidId, 0.0f, 0.0f, false));
+ writer->setForegroundMaterial(position, material, m_materialHue, m_materialColorVariant);
+ if (isRealMod(mod)) {
+ writer->setForegroundMod(position, mod, m_modHue);
+ }
+ }
+
+ BackBrush::BackBrush(String const& material, Maybe<String> mod, Maybe<float> hueshift, Maybe<float> modhueshift, Maybe<MaterialColorVariant> colorVariant) {
+ m_material = material;
+ m_mod = mod;
+ m_materialHue = hueshift.apply(materialHueFromDegrees).value(0);
+ m_modHue = modhueshift.apply(materialHueFromDegrees).value(0);
+ m_materialColorVariant = colorVariant.value(DefaultMaterialColorVariant);
+ }
+
+ void BackBrush::paint(Vec2I position, Phase phase, DungeonGeneratorWriter* writer) const {
+ if (phase != Phase::WallPhase)
+ return;
+
+ auto materialDatabase = Root::singleton().materialDatabase();
+ MaterialId material = materialDatabase->materialId(m_material);
+
+ ModId mod = NoModId;
+ if (m_mod)
+ mod = materialDatabase->modId(*m_mod);
+
+ writer->setBackgroundMaterial(position, material, m_materialHue, m_materialColorVariant);
+ if (isRealMod(mod)) {
+ writer->setBackgroundMod(position, mod, m_modHue);
+ }
+ }
+
+ ObjectBrush::ObjectBrush(String const& object, Star::Direction direction, Json const& parameters) {
+ m_object = object;
+ m_direction = direction;
+ m_parameters = parameters;
+ }
+
+ void ObjectBrush::paint(Vec2I position, Phase phase, DungeonGeneratorWriter* writer) const {
+ if (phase != Phase::ObjectPhase)
+ return;
+ writer->placeObject(position, m_object, m_direction, m_parameters);
+ }
+
+ VehicleBrush::VehicleBrush(String const& vehicle, Json const& parameters) {
+ m_vehicle = vehicle;
+ m_parameters = parameters;
+ }
+
+ void VehicleBrush::paint(Vec2I position, Phase phase, DungeonGeneratorWriter* writer) const {
+ if (phase != Phase::ObjectPhase)
+ return;
+ writer->placeVehicle(Vec2F(position), m_vehicle, m_parameters);
+ }
+
+ void BiomeItemsBrush::paint(Vec2I position, Phase phase, DungeonGeneratorWriter* writer) const {
+ if (phase != Phase::BiomeItemsPhase)
+ return;
+ writer->placeSurfaceBiomeItems(position);
+ }
+
+ void BiomeTreeBrush::paint(Vec2I position, Phase phase, DungeonGeneratorWriter* writer) const {
+ if (phase != Phase::BiomeTreesPhase)
+ return;
+ writer->placeBiomeTree(position);
+ }
+
+ ItemBrush::ItemBrush(ItemDescriptor const& item) : m_item(item) {}
+
+ void ItemBrush::paint(Vec2I position, Phase phase, DungeonGeneratorWriter* writer) const {
+ if (phase != Phase::ItemPhase)
+ return;
+ writer->addDrop(Vec2F(position), m_item);
+ }
+
+ NpcBrush::NpcBrush(Json const& brush) {
+ m_npc = brush;
+ auto map = m_npc.toObject();
+ if (map.value("seed") == Json("stable"))
+ map["seed"] = Random::randu64();
+ m_npc = map;
+ }
+
+ void NpcBrush::paint(Vec2I position, Phase phase, DungeonGeneratorWriter* writer) const {
+ if (phase != Phase::NpcPhase)
+ return;
+
+ if (m_npc.contains("species")) {
+ // interpret species as a comma separated list of unquoted strings
+ StringList speciesOptions = m_npc.get("species").toString().replace(" ", "").split(",");
+ writer->spawnNpc(Vec2F(position), m_npc.set("species", Random::randFrom(speciesOptions)));
+ } else {
+ writer->spawnNpc(Vec2F(position), m_npc);
+ }
+ }
+
+ StagehandBrush::StagehandBrush(Json const& definition) {
+ m_definition = definition;
+ }
+
+ void StagehandBrush::paint(Vec2I position, Phase phase, DungeonGeneratorWriter* writer) const {
+ if (phase != Phase::NpcPhase)
+ return;
+ writer->spawnStagehand(Vec2F(position), m_definition);
+ }
+
+ DungeonIdBrush::DungeonIdBrush(DungeonId dungeonId) {
+ m_dungeonId = dungeonId;
+ }
+
+ void DungeonIdBrush::paint(Vec2I position, Phase phase, DungeonGeneratorWriter* writer) const {
+ if (phase != Phase::DungeonIdPhase)
+ return;
+ writer->setDungeonId(position, m_dungeonId);
+ }
+
+ SurfaceBrush::SurfaceBrush(Maybe<int> variant, Maybe<String> mod) {
+ m_variant = variant.value(0);
+ m_mod = mod;
+ }
+
+ void SurfaceBrush::paint(Vec2I position, Phase phase, DungeonGeneratorWriter* writer) const {
+ if (phase == Phase::WallPhase) {
+ writer->setForegroundMaterial(position, biomeMaterialForJson(m_variant), 0, DefaultMaterialColorVariant);
+ writer->setBackgroundMaterial(position, biomeMaterialForJson(m_variant), 0, DefaultMaterialColorVariant);
+ }
+ if (phase == Phase::ModsPhase) {
+ if (m_mod.isValid()) {
+ auto materialDatabase = Root::singleton().materialDatabase();
+ writer->setForegroundMod(position, materialDatabase->modId(*m_mod), 0);
+ } else {
+ if (writer->needsForegroundBiomeMod(position)) {
+ writer->setForegroundMod(position, BiomeModId, 0);
+ }
+ }
+ }
+ }
+
+ SurfaceBackgroundBrush::SurfaceBackgroundBrush(Maybe<int> variant, Maybe<String> mod) {
+ m_variant = variant.value(0);
+ m_mod = mod;
+ }
+
+ void SurfaceBackgroundBrush::paint(Vec2I position, Phase phase, DungeonGeneratorWriter* writer) const {
+ if (phase == Phase::WallPhase) {
+ writer->setBackgroundMaterial(position, biomeMaterialForJson(m_variant), 0, DefaultMaterialColorVariant);
+ }
+ if (phase == Phase::ModsPhase) {
+ if (m_mod.isValid()) {
+ auto materialDatabase = Root::singleton().materialDatabase();
+ writer->setBackgroundMod(position, materialDatabase->modId(*m_mod), 0);
+ } else {
+ if (writer->needsBackgroundBiomeMod(position)) {
+ writer->setBackgroundMod(position, BiomeModId, 0);
+ }
+ }
+ }
+ }
+
+ LiquidBrush::LiquidBrush(String const& liquidName, float quantity, bool source)
+ : m_liquid(liquidName), m_quantity(quantity), m_source(source) {}
+
+ void LiquidBrush::paint(Vec2I position, Phase phase, DungeonGeneratorWriter* writer) const {
+ auto liquidsDatabase = Root::singleton().liquidsDatabase();
+ LiquidId liquidId = liquidsDatabase->liquidId(m_liquid);
+ LiquidStore liquid(liquidId, m_quantity, 1.0f, m_source);
+ if (phase == Phase::WallPhase) {
+ writer->requestLiquid(position, liquid);
+ }
+ }
+
+ void WireBrush::paint(Vec2I position, Phase phase, DungeonGeneratorWriter* writer) const {
+ if (phase != Phase::WirePhase)
+ return;
+ writer->requestWire(position, m_wireGroup, m_partLocal);
+ }
+
+ void PlayerStartBrush::paint(Vec2I position, Phase phase, DungeonGeneratorWriter* writer) const {
+ if (phase == Phase::NpcPhase)
+ writer->setPlayerStart(Vec2F(position));
+ }
+
+ InvalidBrush::InvalidBrush(Maybe<String> nameHint) : m_nameHint(nameHint) {}
+
+ void InvalidBrush::paint(Vec2I, Phase, DungeonGeneratorWriter*) const {
+ if (m_nameHint)
+ Logger::error("Invalid tile '%s'", *m_nameHint);
+ else
+ Logger::error("Invalid tile");
+ }
+
+ bool Tile::canPlace(Vec2I position, DungeonGeneratorWriter* writer) const {
+ if (writer->otherDungeonPresent(position))
+ return false;
+ else if (position[1] < 0)
+ return false;
+ for (size_t i = 0; i < rules.size(); i++)
+ if (!rules[i]->checkTileCanPlace(position, writer))
+ return false;
+ return true;
+ }
+
+ void Tile::place(Vec2I position, Phase phase, DungeonGeneratorWriter* writer) const {
+ for (size_t i = 0; i < brushes.size(); i++) {
+ brushes[i]->paint(position, phase, writer);
+ }
+ }
+
+ bool Tile::usesPlaces() const {
+ if (brushes.size() == 0)
+ return false;
+ for (size_t i = 0; i < rules.size(); i++)
+ if (rules[i]->overdrawable())
+ return false;
+ return true;
+ }
+
+ bool Tile::modifiesPlaces() const {
+ return brushes.size() != 0;
+ }
+
+ bool Tile::collidesWithPlaces() const {
+ return usesPlaces();
+ }
+
+ bool Tile::requiresOpen() const {
+ for (size_t i = 0; i < rules.size(); i++)
+ if (rules[i]->requiresOpen())
+ return true;
+ return false;
+ }
+
+ bool Tile::requiresSolid() const {
+ for (size_t i = 0; i < rules.size(); i++)
+ if (rules[i]->requiresSolid())
+ return true;
+ return false;
+ }
+
+ bool Tile::requiresLiquid() const {
+ for (size_t i = 0; i < rules.size(); i++)
+ if (rules[i]->requiresLiquid())
+ return true;
+ return false;
+ }
+
+ PartConstPtr parsePart(DungeonDefinition* dungeon, Json const& definition, Maybe<ImageTilesetConstPtr> tileset) {
+ String kind = definition.get("def").getString(0);
+ if (kind == "image") {
+ if (tileset.isNothing())
+ throw DungeonException("Dungeon parts designed in images require the 'tiles' key in the .dungeon file");
+ return make_shared<const Part>(dungeon, definition, make_shared<ImagePartReader>(*tileset));
+ } else if (kind == "tmx")
+ return make_shared<const Part>(dungeon, definition, make_shared<TMXPartReader>());
+ throw DungeonException::format("Unknown dungeon part kind: %s", kind);
+ }
+
+ Part::Part(DungeonDefinition* dungeon, Json const& part, PartReaderPtr reader) {
+ m_dungeon = dungeon;
+ m_name = part.getString("name");
+ m_rules = Rule::readRules(part.get("rules"));
+ m_chance = part.getFloat("chance", 1);
+ if (m_chance <= 0)
+ m_chance = 0.0001f;
+ m_markDungeonId = part.getBool("markDungeonId", true);
+ m_overrideAllowAlways = part.getBool("overrideAllowAlways", false);
+ m_minimumThreatLevel = part.optFloat("minimumThreatLevel");
+ m_maximumThreatLevel = part.optFloat("maximumThreatLevel");
+ m_clearAnchoredObjects = part.getBool("clearAnchoredObjects", true);
+
+ m_reader = reader;
+ Json const& def = part.get("def");
+ if (def.get(1).type() == Json::Type::String) {
+ reader->readAsset(AssetPath::relativeTo(dungeon->directory(), def.get(1).toString()));
+ } else {
+ for (auto const& asset : def.get(1).iterateArray())
+ reader->readAsset(AssetPath::relativeTo(dungeon->directory(), asset.toString()));
+ }
+ m_size = m_reader->size();
+ scanConnectors();
+ scanAnchor();
+ }
+
+ String const& Part::name() const {
+ return m_name;
+ }
+
+ Vec2U Part::size() const {
+ return m_size;
+ }
+
+ Vec2I Part::anchorPoint() const {
+ return m_anchorPoint;
+ }
+
+ float Part::chance() const {
+ return m_chance;
+ }
+
+ bool Part::markDungeonId() const {
+ return m_markDungeonId;
+ }
+
+ Maybe<float> Part::minimumThreatLevel() const {
+ return m_minimumThreatLevel;
+ }
+
+ Maybe<float> Part::maximumThreatLevel() const {
+ return m_maximumThreatLevel;
+ }
+
+ bool Part::clearAnchoredObjects() const {
+ return m_clearAnchoredObjects;
+ }
+
+ int Part::placementLevelConstraint() const {
+ Vec2I air = {0, size().y()};
+ Vec2I ground = {0, 0};
+ Vec2I liquid = {0, 0};
+ m_reader->forEachTile([&ground, &air, &liquid](Vec2I tilePos, Tile const& tile) -> bool {
+ for (auto const& rule : tile.rules) {
+ if (is<WorldGenMustContainSolidRule>(rule) && tilePos.y() > ground.y()) {
+ ground = tilePos;
+ }
+ if (is<WorldGenMustContainAirRule>(rule) && tilePos.y() < air.y()) {
+ air = tilePos;
+ }
+ if ((is<WorldGenMustContainLiquidRule>(rule) || is<WorldGenMustNotContainLiquidRule>(rule)) && tilePos.y() > liquid.y()) {
+ liquid = tilePos;
+ }
+ }
+ return false;
+ });
+ ground[1] = max(ground[1], liquid[1]);
+ if (air.y() < ground.y())
+ throw DungeonException("Invalid ground vs air contraint. Ground at: " + toString(ground.y()) + " Air at: "
+ + toString(air.y())
+ + " Pixels: highest ground:"
+ + toString(ground)
+ + " lowest air:"
+ + toString(air));
+ return air.y();
+ }
+
+ bool Part::ignoresPartMaximum() const {
+ for (size_t i = 0; i < m_rules.size(); i++)
+ if (m_rules[i]->ignorePartMaximum())
+ return true;
+ return false;
+ }
+
+ bool Part::allowsPlacement(int currentPlacementCount) const {
+ for (size_t i = 0; i < m_rules.size(); i++)
+ if (!m_rules[i]->allowSpawnCount(currentPlacementCount))
+ return false;
+ return true;
+ }
+
+ List<ConnectorConstPtr> const& Part::connections() const {
+ return m_connections;
+ }
+
+ bool Part::doesNotConnectTo(Part* part) const {
+ for (size_t i = 0; i < m_rules.size(); i++)
+ if (m_rules[i]->doesNotConnectToPart(part->name()))
+ return true;
+ for (size_t i = 0; i < part->m_rules.size(); i++)
+ if (part->m_rules[i]->doesNotConnectToPart(m_name))
+ return true;
+ return false;
+ }
+
+ bool Part::checkPartCombinationsAllowed(StringMap<int> const& placementCounter) const {
+ for (size_t i = 0; i < m_rules.size(); i++)
+ if (!m_rules[i]->checkPartCombinationsAllowed(placementCounter))
+ return false;
+ return true;
+ }
+
+ bool Part::collidesWithPlaces(Vec2I pos, Set<Vec2I>& places) const {
+ if (m_overrideAllowAlways)
+ return true;
+
+ bool result = false;
+ m_reader->forEachTile([&result, pos, &places](Vec2I tilePos, Tile const& tile) -> bool {
+ if (tile.collidesWithPlaces())
+ if (places.contains(pos + tilePos)) {
+ Logger::debug("Tile collided with place at %s", pos + tilePos);
+ result = true;
+ return true;
+ }
+ return false;
+ });
+
+ return result;
+ }
+
+ bool Part::canPlace(Vec2I pos, DungeonGeneratorWriter* writer) const {
+ if (m_overrideAllowAlways)
+ return true;
+
+ // Speed up repeated failing calls by first checking the tile that failed
+ // last time (if it did).
+ bool result = true;
+ m_reader->forEachTile([&result, pos, writer](Vec2I tilePos, Tile const& tile) -> bool {
+ Vec2I position = pos + tilePos;
+ if (!tile.canPlace(position, writer)) {
+ result = false;
+ return true;
+ }
+ return false;
+ });
+
+ return result;
+ }
+
+ void Part::place(Vec2I pos, Set<Vec2I> const& places, DungeonGeneratorWriter* writer) const {
+ placePhase(pos, Phase::ClearPhase, places, writer);
+ placePhase(pos, Phase::WallPhase, places, writer);
+ placePhase(pos, Phase::ModsPhase, places, writer);
+ placePhase(pos, Phase::ObjectPhase, places, writer);
+ placePhase(pos, Phase::BiomeTreesPhase, places, writer);
+ placePhase(pos, Phase::BiomeItemsPhase, places, writer);
+ placePhase(pos, Phase::WirePhase, places, writer);
+ placePhase(pos, Phase::ItemPhase, places, writer);
+ placePhase(pos, Phase::NpcPhase, places, writer);
+ placePhase(pos, Phase::DungeonIdPhase, places, writer);
+ }
+
+ void Part::forEachTile(TileCallback const& callback) const {
+ m_reader->forEachTile(callback);
+ }
+
+ void Part::placePhase(Vec2I pos, Phase phase, Set<Vec2I> const& places, DungeonGeneratorWriter* writer) const {
+ m_reader->forEachTile([&places, pos, phase, writer](Vec2I tilePos, Tile const& tile) -> bool {
+ Vec2I position = pos + tilePos;
+ if (tile.collidesWithPlaces() || !places.contains(position)) {
+ try {
+ tile.place(position, phase, writer);
+ } catch (std::exception const&) {
+ Logger::error("Error at map position %s:", tilePos);
+ throw;
+ }
+ }
+ return false;
+ });
+ }
+
+ bool Part::tileUsesPlaces(Vec2I pos) const {
+ bool result = false;
+ m_reader->forEachTileAt(pos,
+ [&result](Vec2I, Tile const& tile) -> bool {
+ if (tile.usesPlaces()) {
+ result = true;
+ return true;
+ }
+ return false;
+ });
+ return result;
+ }
+
+ Direction Part::pickByEdge(Vec2I position, Vec2U size) const {
+ int dxa = position[0];
+ int dxb = size[0] - position[0];
+ int dya = position[1];
+ int dyb = size[1] - position[1];
+
+ int m = min(min(dxa, dxb), min(dya, dyb));
+ if (dxa == m)
+ return Direction::Left;
+ if (dxb == m)
+ return Direction::Right;
+ if (dya == m)
+ return Direction::Down;
+ if (dyb == m)
+ return Direction::Up;
+ throw DungeonException("Ambiguous direction");
+ }
+
+ Direction Part::pickByNeighbours(Vec2I pos) const {
+ int x = pos.x(), y = pos.y();
+
+ // if on a border use that, corners use the left/right direction
+ if (x == 0)
+ return Direction::Left;
+ if (x == (int)size().x() - 1)
+ return Direction::Right;
+ if (y == 0)
+ return Direction::Down;
+ if (y == (int)size().y() - 1)
+ return Direction::Up;
+
+ // scans around the connector, the direction where it finds a solid is where
+ // it assume the
+ // connection comes from
+
+ if (tileUsesPlaces({x + 1, y}) && !tileUsesPlaces({x - 1, y}))
+ return Direction::Left;
+
+ if (tileUsesPlaces({x - 1, y}) && !tileUsesPlaces({x + 1, y}))
+ return Direction::Right;
+
+ if (tileUsesPlaces({x, y + 1}) && !tileUsesPlaces({x, y - 1}))
+ return Direction::Down;
+
+ if (tileUsesPlaces({x, y - 1}) && !tileUsesPlaces({x, y + 1}))
+ return Direction::Up;
+
+ return Direction::Unknown;
+ }
+
+ void Part::scanConnectors() {
+ try {
+ m_reader->forEachTile([this](Vec2I position, Tile const& tile) -> bool {
+ if (tile.connector.isValid()) {
+ auto d = tile.connector->direction;
+ if (d == Direction::Unknown)
+ d = pickByNeighbours(position);
+ if (d == Direction::Unknown)
+ d = pickByEdge(position, m_size);
+ Logger::debug("Found connector on %s at %s group %s direction %s", m_name, position, tile.connector->value, (int)d);
+ m_connections.append(make_shared<Connector>(this, tile.connector->value, tile.connector->forwardOnly, d, position));
+ }
+
+ return false;
+ });
+ } catch (std::exception& e) {
+ throw DungeonException(strf("Exception %s in connector %s", outputException(e, true), m_name));
+ }
+ }
+
+ void Part::scanAnchor() {
+ int cx, cy, cc;
+ cx = cy = cc = 0;
+ int lowestAir = m_size[1];
+ int highestGound = -1;
+ int highestLiquid = -1;
+ try {
+ m_reader->forEachTile([&](Vec2I pos, Tile const& tile) -> bool {
+ int x = pos.x(), y = pos.y();
+ if (tile.collidesWithPlaces()) {
+ cx += x;
+ cy += y;
+ cc++;
+ }
+ if (tile.requiresOpen()) {
+ if ((int)y < lowestAir)
+ lowestAir = y;
+ }
+ if (tile.requiresSolid()) {
+ if ((int)y > highestGound)
+ highestGound = y;
+ }
+ if (tile.requiresLiquid()) {
+ if ((int)y > highestLiquid)
+ highestLiquid = y;
+ }
+ return false;
+ });
+ } catch (std::exception& e) {
+ throw DungeonException(strf("Exception %s in part %s", outputException(e, true), m_name));
+ }
+
+ highestGound = max(highestGound, highestLiquid);
+ if (highestGound == -1)
+ highestGound = lowestAir - 1;
+
+ if (lowestAir == (int)m_size[1])
+ lowestAir = highestGound + 1;
+
+ if (cc == 0) {
+ cx = m_size[0] / 2;
+ cy = m_size[1] / 2;
+ } else {
+ cx /= cc;
+ cy /= cc;
+ }
+
+ if (highestGound != -1)
+ cy = highestGound + 1;
+
+ m_anchorPoint = {cx, cy};
+ }
+
+ bool WorldGenMustContainSolidRule::checkTileCanPlace(Vec2I position, DungeonGeneratorWriter* writer) const {
+ return writer->checkSolid(position, layer);
+ }
+
+ bool WorldGenMustContainAirRule::checkTileCanPlace(Vec2I position, DungeonGeneratorWriter* writer) const {
+ return writer->checkOpen(position, layer);
+ }
+
+ bool WorldGenMustContainLiquidRule::checkTileCanPlace(Vec2I position, DungeonGeneratorWriter * writer) const {
+ return writer->checkLiquid(position);
+ }
+
+ bool WorldGenMustNotContainLiquidRule::checkTileCanPlace(Vec2I position, DungeonGeneratorWriter * writer) const {
+ return !writer->checkLiquid(position);
+ }
+
+ Connector::Connector(Part* part, String value, bool forwardOnly, Direction direction, Vec2I offset)
+ : m_value(value), m_forwardOnly(forwardOnly), m_direction(direction), m_offset(offset), m_part(part) {}
+
+ bool Connector::connectsTo(ConnectorConstPtr connector) const {
+ if (m_forwardOnly)
+ return false;
+ if (m_value != connector->m_value)
+ return false;
+ if (m_direction == Direction::Any || connector->m_direction == Direction::Any)
+ return true;
+ if (m_direction != flipDirection(connector->m_direction))
+ return false;
+ return true;
+ }
+
+ String Connector::value() const {
+ return m_value;
+ }
+
+ Vec2I Connector::positionAdjustment() const {
+ if (m_direction == Direction::Any)
+ return Vec2I(0, 0);
+ if (m_direction == Direction::Left)
+ return Vec2I(-1, 0);
+ if (m_direction == Direction::Right)
+ return Vec2I(1, 0);
+ if (m_direction == Direction::Up)
+ return Vec2I(0, 1);
+ starAssert(m_direction == Direction::Down);
+ return Vec2I(0, -1);
+ }
+
+ Part* Connector::part() const {
+ return m_part;
+ }
+
+ Vec2I Connector::offset() const {
+ return m_offset;
+ }
+
+ DungeonGeneratorWriter::DungeonGeneratorWriter(DungeonGeneratorWorldFacadePtr facade, Maybe<int> terrainMarkingSurfaceLevel, Maybe<int> terrainSurfaceSpaceExtends)
+ : m_facade(facade), m_terrainMarkingSurfaceLevel(terrainMarkingSurfaceLevel), m_terrainSurfaceSpaceExtends(terrainSurfaceSpaceExtends) {
+ m_currentBounds.setMin(Vec2I{std::numeric_limits<int32_t>::max(), std::numeric_limits<int32_t>::max()});
+ m_currentBounds.setMax(Vec2I{std::numeric_limits<int32_t>::min(), std::numeric_limits<int32_t>::min()});
+ }
+
+ Vec2I DungeonGeneratorWriter::wrapPosition(Vec2I const& pos) const {
+ return m_facade->getWorldGeometry().xwrap(pos);
+ }
+
+ void DungeonGeneratorWriter::setMarkDungeonId(Maybe<DungeonId> dungeonId) {
+ m_markDungeonId = dungeonId;
+ }
+
+ void DungeonGeneratorWriter::requestLiquid(Vec2I const& pos, LiquidStore const& liquid) {
+ m_pendingLiquids[pos] = liquid;
+ }
+
+ void DungeonGeneratorWriter::setLiquid(Vec2I const& pos, LiquidStore const& liquid) {
+ m_liquids[pos] = liquid;
+ markPosition(pos);
+ }
+
+ void DungeonGeneratorWriter::setForegroundMaterial(Vec2I const& position, MaterialId material, MaterialHue hueshift, MaterialColorVariant colorVariant) {
+ m_foregroundMaterial[position] = {material, hueshift, colorVariant};
+ markPosition(position);
+ }
+
+ void DungeonGeneratorWriter::setBackgroundMaterial(Vec2I const& position, MaterialId material, MaterialHue hueshift, MaterialColorVariant colorVariant) {
+ m_backgroundMaterial[position] = {material, hueshift, colorVariant};
+ markPosition(position);
+ }
+
+ void DungeonGeneratorWriter::setForegroundMod(Vec2I const& position, ModId mod, MaterialHue hueshift) {
+ m_foregroundMod[position] = {mod, hueshift};
+ markPosition(position);
+ }
+
+ void DungeonGeneratorWriter::setBackgroundMod(Vec2I const& position, ModId mod, MaterialHue hueshift) {
+ m_backgroundMod[position] = {mod, hueshift};
+ markPosition(position);
+ }
+
+ bool DungeonGeneratorWriter::needsForegroundBiomeMod(Vec2I const& position) {
+ if (!m_foregroundMaterial.contains(position))
+ return false;
+ if (!isBiomeMaterial(m_foregroundMaterial[position].material))
+ return false;
+ Vec2I abovePosition(position.x(), position.y() + 1);
+ if (m_foregroundMaterial.contains(abovePosition))
+ if (m_foregroundMaterial[abovePosition].material != EmptyMaterialId)
+ return false;
+ return true;
+ }
+
+ bool DungeonGeneratorWriter::needsBackgroundBiomeMod(Vec2I const& position) {
+ if (!m_backgroundMaterial.contains(position))
+ return false;
+ if (!isBiomeMaterial(m_backgroundMaterial[position].material))
+ return false;
+ Vec2I abovePosition(position.x(), position.y() + 1);
+ if (m_backgroundMaterial.contains(abovePosition))
+ if (m_backgroundMaterial[abovePosition].material != EmptyMaterialId)
+ return false;
+ if (m_foregroundMaterial.contains(abovePosition))
+ if (m_foregroundMaterial[abovePosition].material != EmptyMaterialId)
+ return false;
+ return true;
+ }
+
+ void DungeonGeneratorWriter::placeObject(Vec2I const& pos, String const& objectType, Star::Direction direction, Json const& parameters) {
+ m_objects[pos] = {objectType, direction, parameters};
+ markPosition(pos);
+ }
+
+ void DungeonGeneratorWriter::placeVehicle(Vec2F const& pos, String const& vehicleName, Json const& parameters) {
+ m_vehicles[pos] = make_pair(vehicleName, parameters);
+ markPosition(pos);
+ }
+
+ void DungeonGeneratorWriter::placeSurfaceBiomeItems(Vec2I const& pos) {
+ m_biomeItems.insert(pos);
+ markPosition(pos);
+ }
+
+ void DungeonGeneratorWriter::placeBiomeTree(Vec2I const& pos) {
+ m_biomeTrees.insert(pos);
+ markPosition(pos);
+ }
+
+ void DungeonGeneratorWriter::addDrop(Vec2F const& position, ItemDescriptor const& item) {
+ m_drops[position] = item;
+ markPosition(position);
+ }
+
+ void DungeonGeneratorWriter::requestWire(Vec2I const& position, String const& wireGroup, bool partLocal) {
+ if (partLocal)
+ m_openLocalWires[wireGroup].add(position);
+ else
+ m_globalWires[wireGroup].add(position);
+ }
+
+ void DungeonGeneratorWriter::spawnNpc(Vec2F const& position, Json const& definition) {
+ m_npcs[position] = definition;
+ markPosition(position);
+ }
+
+ void DungeonGeneratorWriter::spawnStagehand(Vec2F const& position, Json const& definition) {
+ m_stagehands[position] = definition;
+ markPosition(position);
+ }
+
+ void DungeonGeneratorWriter::setPlayerStart(Vec2F const& startPosition) {
+ m_facade->setPlayerStart(startPosition);
+ }
+
+ bool DungeonGeneratorWriter::checkSolid(Vec2I position, TileLayer layer) {
+ if (m_terrainMarkingSurfaceLevel)
+ return position.y() < *m_terrainMarkingSurfaceLevel;
+ return m_facade->checkSolid(position, layer);
+ }
+
+ bool DungeonGeneratorWriter::checkOpen(Vec2I position, TileLayer layer) {
+ if (m_terrainMarkingSurfaceLevel)
+ return position.y() >= *m_terrainMarkingSurfaceLevel;
+ return m_facade->checkOpen(position, layer);
+ }
+
+ bool DungeonGeneratorWriter::checkLiquid(Vec2I const& position) {
+ return m_facade->checkOceanLiquid(position);
+ }
+
+ bool DungeonGeneratorWriter::otherDungeonPresent(Vec2I position) {
+ return m_facade->getDungeonIdAt(position) != NoDungeonId;
+ }
+
+ void DungeonGeneratorWriter::setDungeonId(Vec2I const& pos, DungeonId dungeonId) {
+ m_dungeonIds[pos] = dungeonId;
+ }
+
+ void DungeonGeneratorWriter::markPosition(Vec2F const& pos) {
+ markPosition(Vec2I(pos.floor()));
+ }
+
+ void DungeonGeneratorWriter::markPosition(Vec2I const& pos) {
+ m_currentBounds.combine(pos);
+ if (m_markDungeonId)
+ m_dungeonIds[pos] = *m_markDungeonId;
+ }
+
+ void DungeonGeneratorWriter::clearTileEntities(RectI const& bounds, Set<Vec2I> const& positions, bool clearAnchoredObjects) {
+ m_facade->clearTileEntities(bounds, positions, clearAnchoredObjects);
+ }
+
+ void DungeonGeneratorWriter::finishPart() {
+ for (auto& entries : m_openLocalWires)
+ m_localWires.append(entries.second);
+ m_openLocalWires.clear();
+
+ if (m_currentBounds.xMin() > m_currentBounds.xMax())
+ return;
+ m_boundingBoxes.push_back(m_currentBounds);
+ m_currentBounds.setMin(Vec2I{std::numeric_limits<int32_t>::max(), std::numeric_limits<int32_t>::max()});
+ m_currentBounds.setMax(Vec2I{std::numeric_limits<int32_t>::min(), std::numeric_limits<int32_t>::min()});
+ }
+
+ void DungeonGeneratorWriter::flushLiquid() {
+ // For each liquid type, find each contiguous region of liquid, then
+ // pressurize that region based on the highest position in the region
+
+ Map<LiquidId, Set<Vec2I>> unpressurizedLiquids;
+ for (auto& p : m_pendingLiquids)
+ unpressurizedLiquids[p.second.liquid].add(p.first);
+
+ for (auto& liquidPair : unpressurizedLiquids) {
+ auto& unpressurized = liquidPair.second;
+ while (!unpressurized.empty()) {
+ // Start with the first unpressurized block as the open set.
+ Vec2I firstBlock = unpressurized.takeFirst();
+ List<Vec2I> openSet = {firstBlock};
+ Set<Vec2I> contiguousRegion = {firstBlock};
+
+ // For each element in the previous open set, add all connected blocks
+ // in
+ // the unpressurized set to the new open set and to the total contiguous
+ // region, taking them from the unpressurized set.
+ while (!openSet.empty()) {
+ auto oldOpenSet = take(openSet);
+ for (auto const& p : oldOpenSet) {
+ for (auto dir : {Vec2I(1, 0), Vec2I(-1, 0), Vec2I(0, 1), Vec2I(0, -1)}) {
+ Vec2I pos = p + dir;
+ if (unpressurized.remove(pos)) {
+ contiguousRegion.add(pos);
+ openSet.append(pos);
+ }
+ }
+ }
+ }
+
+ // Once we have found no more blocks in the unpressurized set to add to
+ // the open set, then we have taken a contiguous region out of the
+ // unpressurized set. Pressurize it based on the highest point.
+ int highestPoint = lowest<int>();
+ for (auto const& p : contiguousRegion)
+ highestPoint = max(highestPoint, p[1]);
+ for (auto const& p : contiguousRegion)
+ m_pendingLiquids[p].pressure = 1.0f + highestPoint - p[1];
+ }
+ }
+
+ for (auto& p : m_pendingLiquids)
+ setLiquid(p.first, p.second);
+
+ m_pendingLiquids.clear();
+ }
+
+ void DungeonGeneratorWriter::flush() {
+ auto geometry = m_facade->getWorldGeometry();
+ auto displace = [&](Vec2I pos) -> Vec2I { return geometry.xwrap(pos); };
+ auto displaceF = [&](Vec2F pos) -> Vec2F { return geometry.xwrap(pos); };
+
+ PolyF::VertexList terrainBlendingVertexes;
+ PolyF::VertexList spaceBlendingVertexes;
+ for (auto bb : m_boundingBoxes) {
+ m_facade->markRegion(bb);
+
+ if (m_terrainMarkingSurfaceLevel) {
+ // Mark the regions of the dungeon above the dungeon surface as needing
+ // space, and the regions below the surface as needing terrain
+ if (bb.yMin() < *m_terrainMarkingSurfaceLevel) {
+ RectI lower = bb;
+ lower.setYMax(min(lower.yMax(), *m_terrainMarkingSurfaceLevel));
+ terrainBlendingVertexes.append(Vec2F(lower.xMin(), lower.yMin()));
+ terrainBlendingVertexes.append(Vec2F(lower.xMax(), lower.yMin()));
+ terrainBlendingVertexes.append(Vec2F(lower.xMin(), lower.yMax()));
+ terrainBlendingVertexes.append(Vec2F(lower.xMax(), lower.yMax()));
+ }
+
+ if (bb.yMax() > *m_terrainMarkingSurfaceLevel) {
+ RectI upper = bb;
+ upper.setYMin(max(upper.yMin(), *m_terrainMarkingSurfaceLevel));
+ spaceBlendingVertexes.append(Vec2F(upper.xMin(), upper.yMin()));
+ spaceBlendingVertexes.append(Vec2F(upper.xMax(), upper.yMin()));
+ spaceBlendingVertexes.append(Vec2F(upper.xMin(), upper.yMax() + m_terrainSurfaceSpaceExtends.value(0)));
+ spaceBlendingVertexes.append(Vec2F(upper.xMax(), upper.yMax() + m_terrainSurfaceSpaceExtends.value(0)));
+ }
+ }
+ }
+
+ if (!terrainBlendingVertexes.empty())
+ m_facade->markTerrain(PolyF::convexHull(terrainBlendingVertexes));
+ if (!spaceBlendingVertexes.empty())
+ m_facade->markSpace(PolyF::convexHull(spaceBlendingVertexes));
+
+ for (auto iter = m_backgroundMaterial.begin(); iter != m_backgroundMaterial.end(); iter++)
+ m_facade->setBackgroundMaterial(displace(iter->first), iter->second.material, iter->second.hueshift, iter->second.colorVariant);
+ for (auto iter = m_foregroundMaterial.begin(); iter != m_foregroundMaterial.end(); iter++)
+ m_facade->setForegroundMaterial(displace(iter->first), iter->second.material, iter->second.hueshift, iter->second.colorVariant);
+ for (auto iter = m_foregroundMod.begin(); iter != m_foregroundMod.end(); iter++)
+ m_facade->setForegroundMod(displace(iter->first), iter->second.mod, iter->second.hueshift);
+ for (auto iter = m_backgroundMod.begin(); iter != m_backgroundMod.end(); iter++)
+ m_facade->setBackgroundMod(displace(iter->first), iter->second.mod, iter->second.hueshift);
+
+ List<Vec2I> sortedPositions = m_objects.keys();
+ sortByComputedValue(sortedPositions, [](Vec2I pos) { return pos[1] + pos[0] / 1000.0f; });
+ for (auto pos : sortedPositions) {
+ auto& object = m_objects[pos];
+ m_facade->placeObject(displace(pos), object.objectName, object.direction, object.parameters);
+ }
+
+ for (auto entry : m_vehicles) {
+ String vehicleName;
+ Json parameters;
+ tie(vehicleName, parameters) = entry.second;
+ m_facade->placeVehicle(displaceF(entry.first), vehicleName, parameters);
+ }
+
+ sortedPositions = List<Vec2I>::from(m_biomeTrees);
+ sortByComputedValue(sortedPositions, [](Vec2I pos) { return pos[1] + pos[0] / 1000.0f; });
+ for (auto pos : sortedPositions) {
+ m_facade->placeBiomeTree(pos);
+ }
+
+ sortedPositions = List<Vec2I>::from(m_biomeItems);
+ sortByComputedValue(sortedPositions, [](Vec2I pos) { return pos[1] + pos[0] / 1000.0f; });
+ for (auto pos : sortedPositions) {
+ m_facade->placeSurfaceBiomeItems(pos);
+ }
+
+ for (auto& npc : m_npcs) {
+ m_facade->spawnNpc(displaceF(npc.first), npc.second);
+ }
+
+ for (auto& stagehand : m_stagehands) {
+ m_facade->spawnStagehand(displaceF(stagehand.first), stagehand.second);
+ }
+
+ for (auto& wires : m_globalWires) {
+ List<Vec2I> wireGroup;
+ for (auto& pos : wires.second)
+ wireGroup.append(displace(pos));
+ m_facade->connectWireGroup(wireGroup);
+ }
+ for (auto& wires : m_localWires) {
+ List<Vec2I> wireGroup;
+ for (auto& pos : wires)
+ wireGroup.append(displace(pos));
+ m_facade->connectWireGroup(wireGroup);
+ }
+
+ for (auto iter = m_drops.begin(); iter != m_drops.end(); iter++)
+ m_facade->addDrop(displaceF(iter->first), iter->second);
+
+ for (auto iter = m_liquids.begin(); iter != m_liquids.end(); iter++)
+ m_facade->setLiquid(displace(iter->first), iter->second);
+
+ for (auto const& dungeonId : m_dungeonIds)
+ m_facade->setDungeonIdAt(dungeonId.first, dungeonId.second);
+ }
+
+ List<RectI> DungeonGeneratorWriter::boundingBoxes() const {
+ return m_boundingBoxes;
+ }
+
+ void DungeonGeneratorWriter::reset() {
+ m_currentBounds.setMin(Vec2I{std::numeric_limits<int32_t>::max(), std::numeric_limits<int32_t>::max()});
+ m_currentBounds.setMax(Vec2I{std::numeric_limits<int32_t>::min(), std::numeric_limits<int32_t>::min()});
+
+ m_pendingLiquids.clear();
+ m_foregroundMaterial.clear();
+ m_backgroundMaterial.clear();
+ m_foregroundMod.clear();
+ m_backgroundMod.clear();
+ m_objects.clear();
+ m_biomeTrees.clear();
+ m_biomeItems.clear();
+ m_drops.clear();
+ m_npcs.clear();
+ m_stagehands.clear();
+ m_liquids.clear();
+ m_globalWires.clear();
+ m_localWires.clear();
+ m_openLocalWires.clear();
+ m_boundingBoxes.clear();
+ }
+}
+
+DungeonDefinitions::DungeonDefinitions() : m_paths(), m_cacheMutex(), m_definitionCache(DefinitionsCacheSize) {
+ auto assets = Root::singleton().assets();
+
+ for (auto file : assets->scan(".dungeon")) {
+ Json dungeon = assets->json(file);
+ m_paths.insert(dungeon.get("metadata").getString("name"), file);
+ }
+}
+
+DungeonDefinitionConstPtr DungeonDefinitions::get(String const& name) const {
+ MutexLocker locker(m_cacheMutex);
+ return m_definitionCache.get(name,
+ [this](String const& name) -> DungeonDefinitionPtr {
+ if (auto path = m_paths.maybe(name))
+ return readDefinition(*path);
+ throw DungeonException::format("Unknown dungeon: '%s'", name);
+ });
+}
+
+JsonObject DungeonDefinitions::getMetadata(String const& name) const {
+ auto definition = get(name);
+ return definition->metadata();
+}
+
+DungeonDefinitionPtr DungeonDefinitions::readDefinition(String const& path) {
+ try {
+ auto assets = Root::singleton().assets();
+ return make_shared<DungeonDefinition>(assets->json(path).toObject(), AssetPath::directory(path));
+ } catch (std::exception const& e) {
+ throw DungeonException::format("Error loading dungeon '%s': %s", path, outputException(e, false));
+ }
+}
+
+DungeonDefinition::DungeonDefinition(JsonObject const& definition, String const& directory) {
+ m_directory = directory;
+ m_metadata = definition.get("metadata").toObject();
+ m_name = m_metadata.get("name").toString();
+ m_displayName = m_metadata.contains("displayName") ? m_metadata.get("displayName").toString() : "";
+ m_species = m_metadata.get("species").toString();
+ m_isProtected = m_metadata.contains("protected") ? m_metadata.get("protected").toBool() : false;
+ if (m_metadata.contains("rules"))
+ m_rules = Dungeon::Rule::readRules(m_metadata.get("rules"));
+
+ m_maxRadius = m_metadata.value("maxRadius", 100).toInt();
+ m_maxParts = m_metadata.value("maxParts", 100).toInt();
+ m_extendSurfaceFreeSpace = m_metadata.value("extendSurfaceFreeSpace", 0).toInt();
+
+ m_anchors = jsonToStringList(m_metadata.get("anchor"));
+
+ auto tileset = definition.maybe("tiles").apply([](Json const& tileset) {
+ return make_shared<const Dungeon::ImageTileset>(tileset);
+ });
+
+ for (auto const& partsDefMap : definition.get("parts").iterateArray()) {
+ Dungeon::PartConstPtr part = parsePart(this, partsDefMap, tileset);
+ if (m_parts.contains(part->name()))
+ throw DungeonException::format("Duplicate dungeon part name: %s", part->name());
+ m_parts.insert(part->name(), part);
+ }
+
+ if (m_metadata.contains("gravity"))
+ m_gravity = m_metadata.get("gravity").toFloat();
+
+ if (m_metadata.contains("breathable"))
+ m_breathable = m_metadata.get("breathable").toBool();
+}
+
+JsonObject DungeonDefinition::metadata() const {
+ return m_metadata;
+}
+
+String DungeonDefinition::directory() const {
+ return m_directory;
+}
+
+String DungeonDefinition::name() const {
+ return m_name;
+}
+
+String DungeonDefinition::displayName() const {
+ return m_displayName;
+}
+
+bool DungeonDefinition::isProtected() const {
+ return m_isProtected;
+}
+
+Maybe<float> DungeonDefinition::gravity() const {
+ return m_gravity;
+}
+
+Maybe<bool> DungeonDefinition::breathable() const {
+ return m_breathable;
+}
+
+StringMap<Dungeon::PartConstPtr> const& DungeonDefinition::parts() const {
+ return m_parts;
+}
+
+List<String> const& DungeonDefinition::anchors() const {
+ return m_anchors;
+}
+
+Maybe<Json> const& DungeonDefinition::optTileset() const {
+ return m_tileset;
+}
+
+int DungeonDefinition::maxParts() const {
+ return m_maxParts;
+}
+
+int DungeonDefinition::maxRadius() const {
+ return m_maxRadius;
+}
+
+int DungeonDefinition::extendSurfaceFreeSpace() const {
+ return m_extendSurfaceFreeSpace;
+}
+
+DungeonGenerator::DungeonGenerator(String const& dungeonName, uint64_t seed, float threatLevel, Maybe<DungeonId> dungeonId)
+ : m_rand(seed), m_threatLevel(threatLevel), m_dungeonId(dungeonId) {
+ m_def = Root::singleton().dungeonDefinitions()->get(dungeonName);
+}
+
+Maybe<pair<List<RectI>, Set<Vec2I>>> DungeonGenerator::generate(DungeonGeneratorWorldFacadePtr facade, Vec2I position, bool markSurfaceAndTerrain, bool forcePlacement) {
+ try {
+ Dungeon::DungeonGeneratorWriter writer(facade, markSurfaceAndTerrain ? position[1] : Maybe<int>(), m_def->extendSurfaceFreeSpace());
+
+ Logger::debug(forcePlacement ? "Forcing generation of dungeon %s" : "Generating dungeon %s", m_def->name());
+
+ Dungeon::PartConstPtr anchor = pickAnchor();
+ if (!anchor) {
+ Logger::error("No valid anchor piece found for dungeon at %s", position);
+ return {};
+ }
+
+ auto pos = position + Vec2I(0, -anchor->placementLevelConstraint());
+ if (forcePlacement || anchor->canPlace(pos, &writer)) {
+ Logger::info("Placing dungeon at %s", position);
+ return buildDungeon(anchor, pos, &writer, forcePlacement);
+ } else {
+ Logger::debug("Failed to place a dungeon at %s", position);
+ return {};
+ }
+ } catch (std::exception const& e) {
+ throw DungeonException(strf("Error generating dungeon named '%s'", m_def->name()), e);
+ }
+}
+
+pair<List<RectI>, Set<Vec2I>> DungeonGenerator::buildDungeon(Dungeon::PartConstPtr anchor, Vec2I basePos, Dungeon::DungeonGeneratorWriter* writer, bool forcePlacement) {
+ writer->reset();
+
+ Deque<std::pair<Dungeon::Part const*, Vec2I>> openSet;
+ StringMap<int> placementCounter;
+ Set<Vec2I> modifiedTiles;
+ Set<Vec2I> preserveTiles;
+ int piecesPlaced = 0;
+
+ Logger::debug("Placing dungeon entrance at %s", basePos);
+
+ auto placePart = [&](Dungeon::Part const* part, Vec2I const& placePos) {
+ Set<Vec2I> clearTileEntityPositions;
+ part->forEachTile([&](Vec2I tilePos, Dungeon::Tile const& tile) -> bool {
+ if (tile.modifiesPlaces())
+ clearTileEntityPositions.insert(writer->wrapPosition(placePos + tilePos));
+ return false;
+ });
+ auto partBounds = RectI::withSize(placePos, Vec2I(part->size()));
+ writer->clearTileEntities(partBounds, clearTileEntityPositions, part->clearAnchoredObjects());
+
+ if (part->markDungeonId())
+ writer->setMarkDungeonId(m_dungeonId);
+ else
+ writer->setMarkDungeonId();
+
+ part->place(placePos, preserveTiles, writer);
+ writer->finishPart();
+
+ part->forEachTile([&](Vec2I tilePos, Dungeon::Tile const& tile) -> bool {
+ if (tile.usesPlaces())
+ preserveTiles.insert(placePos + tilePos);
+ if (tile.modifiesPlaces())
+ modifiedTiles.insert(placePos + tilePos);
+ return false;
+ });
+
+ openSet.append({part, placePos});
+
+ placementCounter[part->name()]++;
+ piecesPlaced++;
+
+ Logger::debug("placed %s", part->name());
+ };
+
+ placePart(anchor.get(), basePos);
+
+ Vec2I origin = basePos + Vec2I(anchor->size()) / 2;
+
+ Set<Vec2I> closedConnectors;
+ while (openSet.size()) {
+ Dungeon::Part const* parentPart = openSet.first().first;
+ Vec2I parentPos = openSet.first().second;
+ openSet.takeFirst();
+ Logger::debug("Trying to add part %s at %s connectors: %s", parentPart->name(), parentPos, parentPart->connections().size());
+ for (size_t i = 0; i < parentPart->connections().size(); i++) {
+ auto connector = parentPart->connections()[i];
+ Vec2I connectorPos = parentPos + connector->offset();
+ if (closedConnectors.contains(connectorPos))
+ continue;
+ List<Dungeon::ConnectorConstPtr> options = findConnectablePart(connector);
+ while (options.size()) {
+ Dungeon::ConnectorConstPtr option = chooseOption(options, m_rand);
+ Logger::debug("Trying part %s", option->part()->name());
+ Vec2I partPos = connectorPos - option->offset() + option->positionAdjustment();
+ Vec2I optionPos = connectorPos + option->positionAdjustment();
+ if (!option->part()->ignoresPartMaximum()) {
+ if (piecesPlaced >= m_def->maxParts())
+ continue;
+
+ if ((partPos - origin).magnitude() > m_def->maxRadius()) {
+ Logger::debug("out of range. %s ... %s", partPos, origin);
+ continue;
+ }
+ }
+ if (!option->part()->allowsPlacement(placementCounter[option->part()->name()])) {
+ Logger::debug("part failed in allowsPlacement");
+ continue;
+ }
+ if (!option->part()->checkPartCombinationsAllowed(placementCounter)) {
+ Logger::debug("part failed in checkPartCombinationsAllowed");
+ continue;
+ }
+ if (option->part()->collidesWithPlaces(partPos, preserveTiles)) {
+ Logger::debug("part failed in collidesWithPlaces");
+ continue;
+ }
+ if (option->part()->minimumThreatLevel() && m_threatLevel < *option->part()->minimumThreatLevel()) {
+ Logger::debug("part failed in minimumThreatLevel");
+ continue;
+ }
+ if (option->part()->maximumThreatLevel() && m_threatLevel > *option->part()->maximumThreatLevel()) {
+ Logger::debug("part failed in maximumThreatLevel");
+ continue;
+ }
+ if (forcePlacement || option->part()->canPlace(partPos, writer)) {
+ placePart(option->part(), partPos);
+ closedConnectors.add(connectorPos);
+ closedConnectors.add(optionPos);
+ break;
+ } else {
+ Logger::debug("part failed in canPlace");
+ }
+ }
+ }
+ }
+ Logger::debug("Settling dungeon water.");
+ writer->flushLiquid();
+ Logger::debug("Flushing dungeon into the worldgen.");
+ writer->flush();
+
+ return {writer->boundingBoxes(), modifiedTiles};
+}
+
+Dungeon::PartConstPtr DungeonGenerator::pickAnchor() {
+ auto validAnchors = m_def->anchors().filtered([this](String const& anchorName) {
+ auto anchorPart = m_def->parts().get(anchorName);
+ return (!anchorPart->minimumThreatLevel() || m_threatLevel >= *anchorPart->minimumThreatLevel())
+ && (!anchorPart->maximumThreatLevel() || m_threatLevel <= *anchorPart->maximumThreatLevel());
+ });
+
+ if (validAnchors.empty())
+ return {};
+
+ return m_def->parts().get(m_rand.randFrom(validAnchors));
+}
+
+List<Dungeon::ConnectorConstPtr> DungeonGenerator::findConnectablePart(Dungeon::ConnectorConstPtr connector) const {
+ List<Dungeon::ConnectorConstPtr> result;
+ for (auto const& partPair : m_def->parts()) {
+ if (partPair.second->doesNotConnectTo(connector->part()))
+ continue;
+ for (auto const& connection : partPair.second->connections()) {
+ if (connection->connectsTo(connector))
+ result.append(connection);
+ }
+ }
+ return result;
+}
+
+DungeonDefinitionConstPtr DungeonGenerator::definition() const {
+ return m_def;
+}
+
+}