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

summaryrefslogtreecommitdiff
path: root/source/game/StarObjectDatabase.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/StarObjectDatabase.cpp
parent6741a057e5639280d85d0f88ba26f000baa58f61 (diff)
everything everywhere
all at once
Diffstat (limited to 'source/game/StarObjectDatabase.cpp')
-rw-r--r--source/game/StarObjectDatabase.cpp593
1 files changed, 593 insertions, 0 deletions
diff --git a/source/game/StarObjectDatabase.cpp b/source/game/StarObjectDatabase.cpp
new file mode 100644
index 0000000..d23b206
--- /dev/null
+++ b/source/game/StarObjectDatabase.cpp
@@ -0,0 +1,593 @@
+#include "StarObjectDatabase.hpp"
+#include "StarObject.hpp"
+#include "StarJsonExtra.hpp"
+#include "StarIterator.hpp"
+#include "StarWorld.hpp"
+#include "StarAssets.hpp"
+#include "StarMaterialDatabase.hpp"
+#include "StarRoot.hpp"
+#include "StarImageMetadataDatabase.hpp"
+#include "StarLogging.hpp"
+#include "StarLoungeableObject.hpp"
+#include "StarContainerObject.hpp"
+#include "StarFarmableObject.hpp"
+#include "StarTeleporterObject.hpp"
+#include "StarPhysicsObject.hpp"
+
+namespace Star {
+
+ObjectOrientation::ParticleEmissionEntry ObjectOrientation::parseParticleEmitter(
+ String const& path, Json const& config) {
+ ObjectOrientation::ParticleEmissionEntry result;
+ result.particleEmissionRate = config.getFloat("emissionRate", 0.0);
+ result.particleEmissionRateVariance = config.getFloat("emissionVariance", 0.0);
+ result.particle = Particle(config.getObject("particle", {}), path);
+ result.particleVariance = Particle(config.getObject("particleVariance", {}), path);
+ result.particle.position += jsonToVec2F(config.get("pixelOrigin", JsonArray{TilePixels / 2, TilePixels / 2})) / TilePixels;
+ result.placeInSpaces = config.getBool("placeInSpaces", false);
+ return result;
+};
+
+bool ObjectOrientation::placementValid(World const* world, Vec2I const& position) const {
+ if (!world)
+ return false;
+
+ for (auto space : spaces) {
+ space += position;
+ if (world->tileIsOccupied(space, TileLayer::Foreground) || world->isTileProtected(space))
+ return false;
+ }
+ return true;
+}
+
+bool ObjectOrientation::anchorsValid(World const* world, Vec2I const& position) const {
+ if (!world)
+ return false;
+
+ if (anchors.size() == 0)
+ return true;
+
+ auto materialDatabase = Root::singleton().materialDatabase();
+
+ auto anchorValid = [&](Anchor const& anchor) -> bool {
+ auto space = position + anchor.position;
+ if (!world->isTileConnectable(space, anchor.layer))
+ return false;
+ if (anchor.tilled) {
+ if (!materialDatabase->isTilledMod(world->mod(space, anchor.layer)))
+ return false;
+ }
+ if (anchor.soil) {
+ if (!materialDatabase->isSoil(world->material(space, anchor.layer)))
+ return false;
+ }
+ if (anchor.material) {
+ if (world->material(space, anchor.layer) != *anchor.material)
+ return false;
+ }
+ return true;
+ };
+
+ bool anyValid = false;
+ for (auto anchor : anchors) {
+ auto valid = anchorValid(anchor);
+ if (valid)
+ anyValid = true;
+ else if (!anchorAny)
+ return false;
+ }
+ return anyValid;
+}
+
+size_t ObjectConfig::findValidOrientation(World const* world, Vec2I const& position, Maybe<Direction> directionAffinity) const {
+ // If we are given a direction affinity, try and find an orientation with a
+ // matching affinity *first*
+ if (directionAffinity) {
+ for (size_t i = 0; i < orientations.size(); ++i) {
+ if (!orientations[i]->directionAffinity || *directionAffinity != *orientations[i]->directionAffinity)
+ continue;
+
+ if (orientations[i]->placementValid(world, position) && orientations[i]->anchorsValid(world, position))
+ return i;
+ }
+ }
+
+ // Then, fallback and try and find any valid affinity
+ for (size_t i = 0; i < orientations.size(); ++i) {
+ if (orientations[i]->placementValid(world, position) && orientations[i]->anchorsValid(world, position))
+ return i;
+ }
+
+ return NPos;
+}
+
+Json ObjectDatabase::parseTouchDamage(String const& path, Json const& config) {
+ auto touchDamage = config.get("touchDamage", {});
+ if (touchDamage.isType(Json::Type::String)) {
+ auto assets = Root::singleton().assets();
+ return assets->fetchJson(AssetPath::relativeTo(path, touchDamage.toString()));
+ }
+
+ return touchDamage;
+}
+
+List<ObjectOrientationPtr> ObjectDatabase::parseOrientations(String const& path, Json const& configList) {
+ auto& root = Root::singleton();
+ auto materialDatabase = root.materialDatabase();
+ List<ObjectOrientationPtr> res;
+ JsonArray configs = configList.toArray();
+
+ // Preprocess the orientation list for config format backwards compatibility.
+ // If dualImage or left/right Image is set, generate two identical
+ // orientations with the appropriate image directives.
+ auto it = makeSMutableIterator(configs);
+ while (it.hasNext()) {
+ JsonObject config = it.next().toObject();
+ if (config.contains("dualImage")) {
+ it.remove();
+
+ JsonObject configLeft = config;
+ configLeft["image"] = config["dualImage"];
+ configLeft["flipImages"] = true;
+ configLeft["direction"] = "left";
+ it.insert(configLeft);
+
+ JsonObject configRight = config;
+ configRight["image"] = config["dualImage"];
+ configRight["direction"] = "right";
+ it.insert(configRight);
+
+ } else if (config.contains("leftImage")) {
+ it.remove();
+
+ JsonObject configLeft = config;
+ configLeft["image"] = config["leftImage"];
+ configLeft["direction"] = "left";
+ it.insert(configLeft);
+
+ JsonObject configRight = config;
+ configRight["image"] = config["rightImage"];
+ configRight["direction"] = "right";
+ it.insert(configRight);
+ }
+ }
+
+ for (auto const& orientationSettings : configs) {
+ auto orientation = make_shared<ObjectOrientation>();
+ orientation->config = orientationSettings;
+
+ if (orientationSettings.contains("imageLayers")) {
+ for (auto layer : orientationSettings.get("imageLayers").iterateArray()) {
+ Drawable drawable(layer.set("centered", layer.getBool("centered", false)));
+ drawable.scale(1.0f / TilePixels);
+ drawable.imagePart().image = AssetPath::relativeTo(path, drawable.imagePart().image);
+ orientation->imageLayers.append(drawable);
+ }
+ } else {
+ Drawable drawable = Drawable::makeImage(
+ AssetPath::relativeTo(path, orientationSettings.getString("image")), 1.0 / TilePixels, false, {});
+ drawable.fullbright = orientationSettings.getBool("fullbright", false);
+ orientation->imageLayers.append(drawable);
+ }
+
+ orientation->renderLayer = parseRenderLayer(orientationSettings.getString("renderLayer", "Object"));
+
+ orientation->flipImages = orientationSettings.getBool("flipImages", false);
+
+ Vec2F imagePosition = jsonToVec2F(orientationSettings.getArray("imagePosition", {0, 0}));
+
+ orientation->imagePosition = imagePosition / TilePixels;
+ orientation->frames = orientationSettings.getInt("frames", 1);
+ orientation->animationCycle = orientationSettings.getDouble("animationCycle", 1.0);
+
+ if (orientationSettings.contains("spaces")) {
+ for (auto v : orientationSettings.getArray("spaces"))
+ orientation->spaces.append(jsonToVec2I(v));
+ } else {
+ orientation->spaces = {{0, 0}};
+ }
+
+ if (orientationSettings.contains("spaceScan")) {
+ auto spaceScanSpaces = Set<Vec2I>::from(orientation->spaces);
+ for (auto const& layer : orientation->imageLayers) {
+ spaceScanSpaces.addAll(root.imageMetadataDatabase()->imageSpaces(
+ layer.imagePart().image.replaceTags(StringMap<String>(), true, "default"),
+ imagePosition,
+ orientationSettings.getDouble("spaceScan"),
+ orientation->flipImages));
+ }
+
+ orientation->spaces = spaceScanSpaces.values();
+ }
+
+ orientation->boundBox = RectI::boundBoxOfPoints(orientation->spaces);
+
+ orientation->metaBoundBox = orientationSettings.opt("metaBoundBox").apply(jsonToRectF);
+
+ // Specify "anchors" to simplify fg / bg anchor listing
+
+ bool tilled = orientationSettings.getBool("requireTilledAnchors", false);
+ bool soil = orientationSettings.getBool("requireSoilAnchors", false);
+ Maybe<MaterialId> anchorMaterial;
+ if (auto anchorMaterialName = orientationSettings.optString("anchorMaterial"))
+ anchorMaterial = materialDatabase->materialId(*anchorMaterialName);
+
+ for (auto type : orientationSettings.getArray("anchors", {})) {
+ String anchorType = type.toString();
+ if (anchorType == "left") {
+ for (auto space : orientation->spaces) {
+ if (space[0] == orientation->boundBox.xMin())
+ orientation->anchors.append({TileLayer::Foreground, space + Vec2I(-1, 0), tilled, soil, anchorMaterial});
+ }
+ } else if (anchorType == "bottom") {
+ for (auto space : orientation->spaces) {
+ if (space[1] == orientation->boundBox.yMin())
+ orientation->anchors.append({TileLayer::Foreground, space + Vec2I(0, -1), tilled, soil, anchorMaterial});
+ }
+ } else if (anchorType == "right") {
+ for (auto space : orientation->spaces) {
+ if (space[0] == orientation->boundBox.xMax())
+ orientation->anchors.append({TileLayer::Foreground, space + Vec2I(1, 0), tilled, soil, anchorMaterial});
+ }
+ } else if (anchorType == "top") {
+ for (auto space : orientation->spaces) {
+ if (space[1] == orientation->boundBox.yMax())
+ orientation->anchors.append({TileLayer::Foreground, space + Vec2I(0, 1), tilled, soil, anchorMaterial});
+ }
+ } else if (anchorType == "background") {
+ for (auto space : orientation->spaces)
+ orientation->anchors.append({TileLayer::Background, space, tilled, soil, anchorMaterial});
+ } else {
+ throw ObjectException(strf("Unknown anchor type: %s", anchorType));
+ }
+ }
+
+ for (auto v : orientationSettings.getArray("bgAnchors", {}))
+ orientation->anchors.append({TileLayer::Background, jsonToVec2I(v), tilled, soil, anchorMaterial});
+
+ for (auto v : orientationSettings.getArray("fgAnchors", {}))
+ orientation->anchors.append({TileLayer::Foreground, jsonToVec2I(v), tilled, soil, anchorMaterial});
+
+ orientation->anchorAny = orientationSettings.getBool("anchorAny", false);
+
+ if (orientationSettings.contains("direction"))
+ orientation->directionAffinity = DirectionNames.getLeft(orientationSettings.getString("direction", "left"));
+
+ auto collisionType = orientationSettings.getString("collision", "none");
+ if (orientationSettings.contains("materialSpaces")) {
+ for (auto space : orientationSettings.get("materialSpaces").iterateArray()) {
+ String materialName = space.get(1).toString();
+ orientation->materialSpaces.append({jsonToVec2I(space.get(0)), materialDatabase->materialId(materialName)});
+ }
+ } else if (collisionType == "solid") {
+ if (orientationSettings.contains("collisionSpaces")) {
+ for (auto space : orientationSettings.get("collisionSpaces").iterateArray())
+ orientation->materialSpaces.append({jsonToVec2I(space), ObjectSolidMaterialId});
+ } else {
+ for (auto space : orientation->spaces)
+ orientation->materialSpaces.append({space, ObjectSolidMaterialId});
+ }
+ } else if (collisionType == "platform") {
+ if (orientationSettings.contains("collisionSpaces")) {
+ for (auto space : orientationSettings.get("collisionSpaces").iterateArray())
+ orientation->materialSpaces.append({jsonToVec2I(space), ObjectPlatformMaterialId});
+ } else {
+ for (auto space : orientation->spaces) {
+ if (space[1] == orientation->boundBox.yMax())
+ orientation->materialSpaces.append({space, ObjectPlatformMaterialId});
+ }
+ }
+ }
+
+ if (orientationSettings.contains("interactiveSpaces")) {
+ List<Vec2I> iSpaces;
+ for (auto space : orientationSettings.get("interactiveSpaces").iterateArray())
+ iSpaces.append(jsonToVec2I(space));
+ orientation->interactiveSpaces = iSpaces;
+ }
+
+ orientation->lightPosition = jsonToVec2F(orientationSettings.getArray("lightPosition", {0, 0}));
+ orientation->beamAngle = orientationSettings.getFloat("beamAngle", 0.0f) * Constants::deg2rad;
+
+ if (orientationSettings.contains("particleEmitter"))
+ orientation->particleEmitters.append(
+ ObjectOrientation::parseParticleEmitter(path, orientationSettings.get("particleEmitter")));
+ for (auto particleEmitterConfig : orientationSettings.getArray("particleEmitters", {}))
+ orientation->particleEmitters.append(ObjectOrientation::parseParticleEmitter(path, particleEmitterConfig));
+
+ orientation->statusEffectArea = orientationSettings.opt("statusEffectArea").apply(jsonToPolyF);
+
+ orientation->touchDamageConfig = parseTouchDamage(path, orientationSettings);
+
+ res.append(move(orientation));
+ }
+
+ return res;
+}
+
+ObjectDatabase::ObjectDatabase() {
+ auto assets = Root::singleton().assets();
+
+ auto files = assets->scanExtension("object");
+ assets->queueJsons(files);
+ for (auto file : files) {
+ try {
+ String name = assets->json(file).getString("objectName");
+ if (m_paths.contains(name))
+ Logger::error("Object %s defined twice, second time from %s", name, file);
+ else
+ m_paths[name] = file;
+ } catch (std::exception const& e) {
+ Logger::error("Error loading object file %s: %s", file, outputException(e, true));
+ }
+ }
+}
+
+void ObjectDatabase::cleanup() {
+ MutexLocker locker(m_cacheMutex);
+ m_configCache.cleanup([](String const&, ObjectConfigPtr const& config) {
+ return !config.unique();
+ });
+}
+
+StringList ObjectDatabase::allObjects() const {
+ return m_paths.keys();
+}
+
+bool ObjectDatabase::isObject(String const& objectName) const {
+ return m_paths.contains(objectName);
+}
+
+ObjectConfigPtr ObjectDatabase::getConfig(String const& objectName) const {
+ MutexLocker locker(m_cacheMutex);
+ return m_configCache.get(objectName,
+ [this](String const& objectName) -> ObjectConfigPtr {
+ if (auto path = m_paths.maybe(objectName))
+ return readConfig(*path);
+ throw ObjectException(strf("No such object named '%s'", objectName));
+ });
+}
+
+List<ObjectOrientationPtr> const& ObjectDatabase::getOrientations(String const& objectName) const {
+ return getConfig(objectName)->orientations;
+}
+
+ObjectPtr ObjectDatabase::createObject(String const& objectName, Json const& parameters) const {
+ auto config = getConfig(objectName);
+
+ if (config->type == "object") {
+ return make_shared<Object>(config, parameters);
+ } else if (config->type == "loungeable") {
+ return make_shared<LoungeableObject>(config, parameters);
+ } else if (config->type == "container") {
+ return make_shared<ContainerObject>(config, parameters);
+ } else if (config->type == "farmable") {
+ return make_shared<FarmableObject>(config, parameters);
+ } else if (config->type == "teleporter") {
+ return make_shared<TeleporterObject>(config, parameters);
+ } else if (config->type == "physics") {
+ return make_shared<PhysicsObject>(config, parameters);
+ } else {
+ throw ObjectException(strf("Unknown objectType '%s' constructing object '%s'", config->type, objectName));
+ }
+}
+
+ObjectPtr ObjectDatabase::diskLoadObject(Json const& diskStore) const {
+ auto object = createObject(diskStore.getString("name"), diskStore.get("parameters"));
+ object->readStoredData(diskStore);
+ object->setNetStates();
+ return object;
+}
+
+ObjectPtr ObjectDatabase::netLoadObject(ByteArray const& netStore) const {
+ DataStreamBuffer ds(netStore);
+ String name = ds.read<String>();
+ Json parameters = ds.read<Json>();
+ return createObject(name, parameters);
+}
+
+bool ObjectDatabase::canPlaceObject(World const* world, Vec2I const& position, String const& objectName) const {
+ return getConfig(objectName)->findValidOrientation(world, position) != NPos;
+}
+
+ObjectPtr ObjectDatabase::createForPlacement(World const* world, String const& objectName, Vec2I const& position,
+ Direction direction, Json const& parameters) const {
+ if (!canPlaceObject(world, position, objectName))
+ return {};
+
+ ObjectPtr object = createObject(objectName, parameters);
+ object->setTilePosition(world->geometry().xwrap(position));
+ object->setDirection(direction);
+
+ return object;
+}
+
+ObjectConfigPtr ObjectDatabase::readConfig(String const& path) {
+ try {
+ auto assets = Root::singleton().assets();
+
+ Json config = assets->json(path);
+
+ auto objectConfig = make_shared<ObjectConfig>();
+ objectConfig->path = path;
+ objectConfig->config = config;
+
+ objectConfig->name = config.getString("objectName");
+ objectConfig->type = config.getString("objectType", "object");
+ objectConfig->race = config.getString("race", "generic");
+ objectConfig->category = config.getString("category", "other");
+ objectConfig->colonyTags = jsonToStringList(config.get("colonyTags", JsonArray()));
+
+ objectConfig->scripts = jsonToStringList(config.get("scripts", JsonArray())).transformed(bind(AssetPath::relativeTo, path, _1));
+ objectConfig->animationScripts = jsonToStringList(config.get("animationScripts", JsonArray())).transformed(bind(AssetPath::relativeTo, path, _1));
+
+ objectConfig->price = config.getInt("price", 0);
+ if (objectConfig->price == 0)
+ objectConfig->price = 1;
+
+ objectConfig->hasObjectItem = config.getBool("hasObjectItem", true);
+
+ objectConfig->scannable = config.getBool("scannable", true);
+ objectConfig->printable = objectConfig->hasObjectItem && config.getBool("printable", objectConfig->scannable);
+
+ objectConfig->retainObjectParametersInItem = config.getBool("retainObjectParametersInItem", false);
+
+ if (config.contains("breakDropPool"))
+ objectConfig->breakDropPool = config.getString("breakDropPool");
+
+ if (config.contains("breakDropOptions")) {
+ for (auto dropChoiceGroups : config.get("breakDropOptions").iterateArray()) {
+ List<ItemDescriptor> group;
+ for (auto dropChoiceEntry : dropChoiceGroups.iterateArray())
+ group.append(
+ {dropChoiceEntry.getString(0), (size_t)dropChoiceEntry.getUInt(1), dropChoiceEntry.getObject(2)});
+ objectConfig->breakDropOptions.append(group);
+ }
+ // If breakDropOptions is set but empty, then the object should always
+ // drop nothing.
+ if (objectConfig->breakDropOptions.empty())
+ objectConfig->breakDropOptions.append({});
+ }
+
+ if (config.contains("smashDropPool"))
+ objectConfig->smashDropPool = config.getString("smashDropPool");
+
+ for (auto dropChoiceGroups : config.get("smashDropOptions", JsonArray()).iterateArray()) {
+ List<ItemDescriptor> group;
+ for (auto dropChoiceEntry : dropChoiceGroups.iterateArray())
+ group.append(ItemDescriptor(dropChoiceEntry));
+ objectConfig->smashDropOptions.append(group);
+ }
+
+ for (auto& sound : config.get("smashSounds", JsonArray()).iterateArray())
+ objectConfig->smashSoundOptions.append(AssetPath::relativeTo(path, sound.toString()));
+
+ if (config.contains("smashParticles"))
+ objectConfig->smashParticles = config.getArray("smashParticles");
+
+ objectConfig->smashable = config.getBool("smashable", false);
+
+ objectConfig->smashOnBreak = config.getBool("smashOnBreak", objectConfig->smashable);
+
+ objectConfig->unbreakable = config.getBool("unbreakable", false);
+ if (objectConfig->unbreakable)
+ objectConfig->smashable = false;
+
+ objectConfig->tileDamageParameters = TileDamageParameters(
+ assets->fetchJson(config.get("damageTable", "/objects/defaultParameters.config:damageTable")),
+ config.optFloat("health"),
+ config.optUInt("harvestLevel"));
+
+ objectConfig->damageShakeMagnitude = config.getFloat("damageShakeMagnitude", 0.2f);
+ objectConfig->damageMaterialKind = config.getString("damageMaterialKind", "solid");
+
+ if (config.contains("damageTeam")) {
+ auto damageTeam = config.get("damageTeam");
+ objectConfig->damageTeam.type = TeamTypeNames.getLeft(damageTeam.getString("type", "environment"));
+ objectConfig->damageTeam.team = damageTeam.getUInt("team", 0);
+ }
+
+ if (config.contains("lightColor")) {
+ objectConfig->lightColors["default"] = jsonToColor(config.get("lightColor"));
+ } else if (config.contains("lightColors")) {
+ for (auto const& pair : config.get("lightColors").iterateObject())
+ objectConfig->lightColors[pair.first] = jsonToColor(pair.second);
+ }
+
+ objectConfig->pointLight = config.getBool("pointLight", false);
+ objectConfig->pointBeam = config.getFloat("pointBeam", 0.0f);
+ objectConfig->beamAmbience = config.getFloat("beamAmbience", 0.0f);
+
+ if (config.contains("flickerPeriod")) {
+ objectConfig->lightFlickering = PeriodicFunction<float>(config.getFloat("flickerPeriod"),
+ config.getFloat("flickerMinIntensity", 0.0),
+ config.getFloat("flickerMaxIntensity", 0.0),
+ config.getFloat("flickerPeriodVariance", 0.0),
+ config.getFloat("flickerIntensityVariance", 0.0));
+ }
+
+ objectConfig->soundEffect = config.getString("soundEffect", "");
+ objectConfig->soundEffectRangeMultiplier = config.getFloat("soundEffectRangeMultiplier", 1.0f);
+
+ objectConfig->statusEffects = config.getArray("statusEffects", {}).transformed(jsonToPersistentStatusEffect);
+ objectConfig->touchDamageConfig = parseTouchDamage(path, config);
+
+ objectConfig->minimumLiquidLevel = config.optFloat("minimumLiquidLevel");
+ objectConfig->maximumLiquidLevel = config.optFloat("maximumLiquidLevel");
+ objectConfig->liquidCheckInterval = config.getFloat("liquidCheckInterval", 0.5);
+
+ objectConfig->health = config.getFloat("health", 1);
+
+ if (auto animationConfig = config.get("animation", {})) {
+ objectConfig->animationConfig = assets->fetchJson(animationConfig, path);
+ if (auto customConfig = config.get("animationCustom", {}))
+ objectConfig->animationConfig = jsonMerge(objectConfig->animationConfig, assets->fetchJson(customConfig, path));
+ }
+
+ objectConfig->orientations = ObjectDatabase::parseOrientations(path, config.get("orientations"));
+
+ // For compatibility, allow particle emitter specs in the base config as
+ // well as in individual orientations.
+
+ List<ObjectOrientation::ParticleEmissionEntry> particleEmitters;
+ if (config.contains("particleEmitter"))
+ particleEmitters.append(ObjectOrientation::parseParticleEmitter(path, config.get("particleEmitter")));
+ for (auto particleEmitterConfig : config.getArray("particleEmitters", {}))
+ particleEmitters.append(ObjectOrientation::parseParticleEmitter(path, particleEmitterConfig));
+
+ for (auto orientation : objectConfig->orientations)
+ orientation->particleEmitters.appendAll(particleEmitters);
+
+ objectConfig->rooting = config.getBool("rooting", false);
+
+ objectConfig->biomePlaced = config.getBool("biomePlaced", false);
+
+ return objectConfig;
+ } catch (std::exception const& e) {
+ throw ObjectException::format("Error loading object '%s': %s", path, outputException(e, false));
+ }
+}
+
+List<Drawable> ObjectDatabase::cursorHintDrawables(World const* world, String const& objectName, Vec2I const& position,
+ Direction direction, Json parameters) const {
+ List<Drawable> drawables;
+
+ auto config = getConfig(objectName);
+ parameters = jsonMerge(config->config, parameters);
+
+ if (auto placementImage = parameters.optString("placementImage")) {
+ if (direction == Direction::Left)
+ *placementImage += "?flipx";
+ drawables = {Drawable::makeImage(AssetPath::relativeTo(config->path, *placementImage),
+ 1.0 / TilePixels, false, Vec2F(position) + jsonToVec2F(parameters.get("placementImagePosition")) / TilePixels)};
+ } else {
+ size_t orientationIndex = config->findValidOrientation(world, position, direction);
+ if (orientationIndex == NPos) {
+ // If we aren't in a valid orientation, still need to draw something at
+ // the cursor. Draw the first orientation whose direction affinity
+ // matches our current direction, or if that fails just the first
+ // orientation.
+ List<Drawable> result;
+ for (size_t i = 0; i < config->orientations.size(); ++i) {
+ if (config->orientations[i]->directionAffinity == direction)
+ orientationIndex = i;
+ }
+ if (orientationIndex == NPos)
+ orientationIndex = 0;
+ }
+
+ auto orientation = config->orientations.at(orientationIndex);
+ for (auto const& layer : orientation->imageLayers) {
+ auto drawable = layer;
+ drawable.imagePart().image = drawable.imagePart().image.replaceTags(StringMap<String>(), true, "default");
+ if (orientation->flipImages)
+ drawable.scale(Vec2F(-1, 1), drawable.boundBox(false).center() - drawable.position);
+ drawables.append(move(drawable));
+ }
+ Drawable::translateAll(drawables, Vec2F(position) + orientation->imagePosition);
+ }
+
+ return drawables;
+}
+
+}