diff options
author | Kae <80987908+Novaenia@users.noreply.github.com> | 2023-06-20 14:33:09 +1000 |
---|---|---|
committer | Kae <80987908+Novaenia@users.noreply.github.com> | 2023-06-20 14:33:09 +1000 |
commit | 6352e8e3196f78388b6c771073f9e03eaa612673 (patch) | |
tree | e23772f79a7fbc41bc9108951e9e136857484bf4 /source/game/StarObjectDatabase.cpp | |
parent | 6741a057e5639280d85d0f88ba26f000baa58f61 (diff) |
everything everywhere
all at once
Diffstat (limited to 'source/game/StarObjectDatabase.cpp')
-rw-r--r-- | source/game/StarObjectDatabase.cpp | 593 |
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; +} + +} |