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

summaryrefslogtreecommitdiff
path: root/source/game/StarWorldClient.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/StarWorldClient.cpp
parent6741a057e5639280d85d0f88ba26f000baa58f61 (diff)
everything everywhere
all at once
Diffstat (limited to 'source/game/StarWorldClient.cpp')
-rw-r--r--source/game/StarWorldClient.cpp1941
1 files changed, 1941 insertions, 0 deletions
diff --git a/source/game/StarWorldClient.cpp b/source/game/StarWorldClient.cpp
new file mode 100644
index 0000000..87fc590
--- /dev/null
+++ b/source/game/StarWorldClient.cpp
@@ -0,0 +1,1941 @@
+#include "StarWorldClient.hpp"
+#include "StarIterator.hpp"
+#include "StarLogging.hpp"
+#include "StarBiome.hpp"
+#include "StarMaterialRenderProfile.hpp"
+#include "StarLiquidTypes.hpp"
+#include "StarDamageDatabase.hpp"
+#include "StarParticleDatabase.hpp"
+#include "StarParticleManager.hpp"
+#include "StarWorldImpl.hpp"
+#include "StarPlayer.hpp"
+#include "StarPlayerLog.hpp"
+#include "StarAggressiveEntity.hpp"
+#include "StarPhysicsEntity.hpp"
+#include "StarItemDrop.hpp"
+#include "StarItemDatabase.hpp"
+#include "StarObjectDatabase.hpp"
+#include "StarObject.hpp"
+#include "StarEntityFactory.hpp"
+#include "StarWorldTemplate.hpp"
+#include "StarStoredFunctions.hpp"
+#include "StarInspectableEntity.hpp"
+
+namespace Star {
+
+const float WorldClient::DropDist = 6.0f;
+
+WorldClient::WorldClient(PlayerPtr mainPlayer) {
+ auto& root = Root::singleton();
+ auto assets = root.assets();
+
+ m_clientConfig = assets->json("/client.config");
+
+ m_currentStep = 0;
+ m_fullBright = false;
+ m_worldDimTimer = GameTimer(m_clientConfig.getFloat("worldDimTime"));
+ m_worldDimTimer.setDone();
+ m_worldDimLevel = 0.0f;
+
+ m_parallaxFadeTimer = GameTimer(m_clientConfig.getFloat("parallaxFadeTime"));
+ m_parallaxFadeTimer.setDone();
+
+ m_collisionDebug = false;
+ m_inWorld = false;
+
+ m_mainPlayer = mainPlayer;
+
+ centerClientWindowOnPlayer(Vec2U(100, 100));
+
+ m_collisionGenerator.init([this](int x, int y) {
+ return m_tileArray->tile({x, y}).collision;
+ });
+
+ m_modifiedTilePredictionTimeout = (int)round(m_clientConfig.getFloat("modifiedTilePredictionTimeout") / WorldTimestep);
+
+ m_latency = 0.0;
+
+ m_blockDamageParticle = Particle(m_clientConfig.getObject("blockDamageParticle"));
+ m_blockDamageParticleVariance = Particle(m_clientConfig.getObject("blockDamageParticleVariance"));
+ m_blockDamageParticleProbability = m_clientConfig.getFloat("blockDamageParticleProbability");
+
+ m_blockDingParticle = Particle(m_clientConfig.getObject("blockDingParticle"));
+ m_blockDingParticleVariance = Particle(m_clientConfig.getObject("blockDingParticleVariance"));
+ m_blockDingParticleProbability = m_clientConfig.getFloat("blockDingParticleProbability");
+
+ m_damageNotificationBatchDuration = m_clientConfig.getFloat("damageNotificationBatchDuration");
+
+ m_ambientSounds.setTrackFadeInTime(assets->json("/interface.config:ambientTrackFadeInTime").toFloat());
+ m_ambientSounds.setTrackSwitchGrace(assets->json("/interface.config:ambientTrackSwitchGrace").toFloat());
+
+ m_musicTrack.setTrackSwitchGrace(assets->json("/interface.config:musicTrackSwitchGrace").toFloat());
+ m_musicTrack.setTrackFadeInTime(assets->json("/interface.config:musicTrackFadeInTime").toFloat());
+
+ m_altMusicTrack.setTrackFadeInTime(assets->json("/interface.config:musicTrackFadeInTime").toFloat());
+ m_altMusicTrack.setTrackSwitchGrace(assets->json("/interface.config:musicTrackFadeInTime").toFloat());
+ m_altMusicTrack.setVolume(0, 0, 0);
+ m_altMusicActive = false;
+
+ clearWorld();
+}
+
+WorldClient::~WorldClient() {
+ clearWorld();
+}
+
+bool WorldClient::inWorld() const {
+ return m_inWorld;
+}
+
+bool WorldClient::inSpace() const {
+ if (!m_sky)
+ return false;
+ return m_sky->inSpace();
+}
+
+bool WorldClient::flying() const {
+ if (!m_sky)
+ return false;
+ return m_sky->flying();
+}
+
+bool WorldClient::mainPlayerDead() const {
+ if (inWorld())
+ return !m_entityMap->get<Player>(m_mainPlayer->entityId());
+ else
+ return false;
+}
+
+void WorldClient::reviveMainPlayer() {
+ if (inWorld() && mainPlayerDead()) {
+ m_mainPlayer->revive(m_playerStart);
+ m_mainPlayer->init(this, m_entityMap->reserveEntityId(), EntityMode::Master);
+ m_entityMap->addEntity(m_mainPlayer);
+ }
+}
+
+bool WorldClient::respawnInWorld() const {
+ return m_respawnInWorld;
+}
+
+WorldTemplateConstPtr WorldClient::currentTemplate() const {
+ return m_worldTemplate;
+}
+
+SkyConstPtr WorldClient::currentSky() const {
+ return m_sky;
+}
+
+void WorldClient::timer(int stepsDelay, WorldAction worldAction) {
+ if (!inWorld())
+ return;
+
+ m_timers.append({stepsDelay, worldAction});
+}
+
+EntityPtr WorldClient::closestEntity(Vec2F const& center, float radius, EntityFilter selector) const {
+ if (!inWorld())
+ return {};
+
+ return m_entityMap->closestEntity(center, radius, selector);
+}
+
+void WorldClient::forAllEntities(EntityCallback callback) const {
+ m_entityMap->forAllEntities(callback);
+}
+
+void WorldClient::forEachEntity(RectF const& boundBox, EntityCallback callback) const {
+ if (!inWorld())
+ return;
+ m_entityMap->forEachEntity(boundBox, callback);
+}
+
+void WorldClient::forEachEntityLine(Vec2F const& begin, Vec2F const& end, EntityCallback callback) const {
+ if (!inWorld())
+ return;
+ m_entityMap->forEachEntityLine(begin, end, callback);
+}
+
+void WorldClient::forEachEntityAtTile(Vec2I const& pos, EntityCallbackOf<TileEntity> callback) const {
+ if (!inWorld())
+ return;
+ m_entityMap->forEachEntityAtTile(pos, callback);
+}
+
+EntityPtr WorldClient::findEntity(RectF const& boundBox, EntityFilter entityFilter) const {
+ if (!inWorld())
+ return {};
+ return m_entityMap->findEntity(boundBox, entityFilter);
+}
+
+EntityPtr WorldClient::findEntityLine(Vec2F const& begin, Vec2F const& end, EntityFilter entityFilter) const {
+ if (!inWorld())
+ return {};
+ return m_entityMap->findEntityLine(begin, end, entityFilter);
+}
+
+EntityPtr WorldClient::findEntityAtTile(Vec2I const& pos, EntityFilterOf<TileEntity> entityFilter) const {
+ if (!inWorld())
+ return {};
+ return m_entityMap->findEntityAtTile(pos, entityFilter);
+}
+
+bool WorldClient::tileIsOccupied(Vec2I const& pos, TileLayer layer, bool includeEphemeral) const {
+ if (!inWorld())
+ return false;
+ return WorldImpl::tileIsOccupied(m_tileArray, m_entityMap, pos, layer, includeEphemeral);
+}
+
+void WorldClient::forEachCollisionBlock(RectI const& region, function<void(CollisionBlock const&)> const& iterator) const {
+ if (!inWorld())
+ return;
+
+ const_cast<WorldClient*>(this)->freshenCollision(region);
+ m_tileArray->tileEach(region, [iterator](Vec2I const& pos, ClientTile const& tile) {
+ if (tile.collision == CollisionKind::Null) {
+ iterator(CollisionBlock::nullBlock(pos));
+ } else {
+ starAssert(!tile.collisionCacheDirty);
+ for (auto const& block : tile.collisionCache)
+ iterator(block);
+ }
+ });
+}
+
+bool WorldClient::isTileConnectable(Vec2I const& pos, TileLayer layer, bool tilesOnly) const {
+ if (!inWorld())
+ return false;
+
+ return m_tileArray->tile(pos).isConnectable(layer, tilesOnly);
+}
+
+bool WorldClient::pointTileCollision(Vec2F const& point, CollisionSet const& collisionSet) const {
+ if (!inWorld())
+ return false;
+
+ return m_tileArray->tile(Vec2I(point.floor())).isColliding(collisionSet);
+}
+
+bool WorldClient::lineTileCollision(Vec2F const& begin, Vec2F const& end, CollisionSet const& collisionSet) const {
+ if (!inWorld())
+ return false;
+
+ return WorldImpl::lineTileCollision(m_geometry, m_tileArray, begin, end, collisionSet);
+}
+
+Maybe<pair<Vec2F, Vec2I>> WorldClient::lineTileCollisionPoint(Vec2F const& begin, Vec2F const& end, CollisionSet const& collisionSet) const {
+ if (!inWorld())
+ return {};
+
+ return WorldImpl::lineTileCollisionPoint(m_geometry, m_tileArray, begin, end, collisionSet);
+}
+
+List<Vec2I> WorldClient::collidingTilesAlongLine(
+ Vec2F const& begin, Vec2F const& end, CollisionSet const& collisionSet, int maxSize, bool includeEdges) const {
+ if (!inWorld())
+ return {};
+
+ return WorldImpl::collidingTilesAlongLine(m_geometry, m_tileArray, begin, end, collisionSet, maxSize, includeEdges);
+}
+
+bool WorldClient::rectTileCollision(RectI const& region, CollisionSet const& collisionSet) const {
+ if (!inWorld())
+ return false;
+
+ return WorldImpl::rectTileCollision(m_tileArray, region, collisionSet);
+}
+
+LiquidLevel WorldClient::liquidLevel(Vec2I const& pos) const {
+ if (!inWorld())
+ return {};
+ return m_tileArray->tile(pos).liquid;
+}
+
+LiquidLevel WorldClient::liquidLevel(RectF const& region) const {
+ if (!inWorld())
+ return {};
+ return WorldImpl::liquidLevel(m_tileArray, region);
+}
+
+TileModificationList WorldClient::validTileModifications(TileModificationList const& modificationList, bool allowEntityOverlap) const {
+ if (!inWorld())
+ return {};
+
+ return WorldImpl::splitTileModifications(m_tileArray, m_entityMap, modificationList, allowEntityOverlap, [this](Vec2I pos, TileModification) {
+ return !m_predictedTiles.contains(pos) && !isTileProtected(pos);
+ }).first;
+}
+
+TileModificationList WorldClient::applyTileModifications(TileModificationList const& modificationList, bool allowEntityOverlap) {
+ if (!inWorld())
+ return {};
+
+ auto result = WorldImpl::splitTileModifications(m_tileArray, m_entityMap, modificationList, allowEntityOverlap, [this](Vec2I pos, TileModification) {
+ return !m_predictedTiles.contains(pos) && !isTileProtected(pos);
+ });
+
+ if (!result.first.empty()) {
+ for (auto entry : result.first)
+ m_predictedTiles[entry.first] = 0;
+ m_outgoingPackets.append(make_shared<ModifyTileListPacket>(result.first, allowEntityOverlap));
+ }
+
+ return result.second;
+}
+
+float WorldClient::gravity(Vec2F const& pos) const {
+ if (!inWorld())
+ return 0.0f;
+
+ if (m_overrideGravity)
+ return *m_overrideGravity;
+
+ auto dungeonId = m_tileArray->tile(Vec2I::round(pos)).dungeonId;
+ return m_dungeonIdGravity.maybe(dungeonId).value(currentTemplate()->gravity());
+}
+
+float WorldClient::windLevel(Vec2F const& pos) const {
+ if (!inWorld())
+ return 0.0f;
+
+ return WorldImpl::windLevel(m_tileArray, pos, m_weather.wind());
+}
+
+void WorldClient::setClientWindow(RectI window) {
+ m_clientState.setWindow(window);
+}
+
+void WorldClient::centerClientWindowOnPlayer(Vec2U const& windowSize) {
+ setClientWindow(RectI::withCenter(Vec2I::floor(m_mainPlayer->position()), Vec2I(windowSize)));
+}
+
+void WorldClient::centerClientWindowOnPlayer() {
+ centerClientWindowOnPlayer(Vec2U(clientWindow().size()));
+}
+
+RectI WorldClient::clientWindow() const {
+ return m_clientState.window();
+}
+
+void WorldClient::render(WorldRenderData& renderData, unsigned bufferTiles) {
+ renderData.clear();
+ if (!inWorld())
+ return;
+
+ // If we're dimming the world, then that takes priority
+ m_worldDimTimer.tick();
+ float dimRatio = m_worldDimTimer.percent();
+
+ // Spends 80% of the time at pitch black with 10% ramp up and down
+
+ m_worldDimColor = {}; // always reset this to prevent persistent dimming from other sources
+ if (dimRatio) {
+ if (dimRatio <= 0.1f)
+ m_worldDimLevel = dimRatio / 0.1f;
+ else if (dimRatio >= 0.9f)
+ m_worldDimLevel = (1 - dimRatio) / (1 - 0.9f);
+ else
+ m_worldDimLevel = 1.0f;
+ }
+
+ List<LightSource> renderLightSources;
+ List<PreviewTile> previewTiles;
+
+ renderData.geometry = m_geometry;
+
+ float pulseAmount = Root::singleton().assets()->json("/highlights.config:interactivePulseAmount").toFloat();
+ float pulseRate = Root::singleton().assets()->json("/highlights.config:interactivePulseRate").toFloat();
+ float pulseLevel = 1 - pulseAmount * 0.5 * (sin(2 * Constants::pi * pulseRate * Time::monotonicMilliseconds() / 1000.0) + 1);
+
+ bool inspecting = m_mainPlayer->inspecting();
+ float inspectionFlickerMultiplier = Random::randf(1 - Root::singleton().assets()->json("/highlights.config:inspectionFlickerAmount").toFloat(), 1);
+
+ EntityId playerAimInteractive = NullEntityId;
+ if (Root::singleton().configuration()->get("interactiveHighlight").toBool()) {
+ if (auto entity = m_mainPlayer->bestInteractionEntity(false))
+ playerAimInteractive = entity->entityId();
+ }
+
+ Maybe<StringList> directives;
+ if (auto worldTemplate = m_worldTemplate) {
+ if(auto parameters = worldTemplate->worldParameters())
+ directives = m_worldTemplate->worldParameters()->globalDirectives;
+ }
+ m_entityMap->forAllEntities([&](EntityPtr const& entity) {
+ if (m_startupHiddenEntities.contains(entity->entityId()))
+ return;
+
+ ClientRenderCallback renderCallback;
+ entity->render(&renderCallback);
+
+ EntityDrawables ed;
+ for (auto& p : renderCallback.drawables) {
+ if (directives) {
+ int directiveIndex = unsigned(entity->entityId()) % directives->size();
+ for (auto& d : p.second) {
+ if (d.isImage())
+ d.imagePart().addDirectives(directives->at(directiveIndex), true);
+ }
+ }
+ ed.layers[p.first] = move(p.second);
+ }
+
+ if (m_interactiveHighlightMode || (!inspecting && entity->entityId() == playerAimInteractive)) {
+ if (auto interactive = as<InteractiveEntity>(entity)) {
+ if (interactive->isInteractive()) {
+ ed.highlightEffect.type = EntityHighlightEffectType::Interactive;
+ ed.highlightEffect.level = pulseLevel;
+ }
+ }
+ } else if (inspecting) {
+ if (auto inspectable = as<InspectableEntity>(entity)) {
+ ed.highlightEffect = m_mainPlayer->inspectionHighlight(inspectable);
+ ed.highlightEffect.level *= inspectionFlickerMultiplier;
+ }
+ }
+ renderData.entityDrawables.append(move(ed));
+
+ renderLightSources.appendAll(move(renderCallback.lightSources));
+
+ if (directives) {
+ int directiveIndex = unsigned(entity->entityId()) % directives->size();
+ for (auto& p : renderCallback.particles)
+ p.directives = directives->get(directiveIndex);
+ }
+
+ m_particles->addParticles(move(renderCallback.particles));
+ m_samples.appendAll(move(renderCallback.audios));
+ previewTiles.appendAll(move(renderCallback.previewTiles));
+ renderData.overheadBars.appendAll(move(renderCallback.overheadBars));
+
+ }, [](EntityPtr const& a, EntityPtr const& b) {
+ return a->entityId() < b->entityId();
+ });
+
+ RectI lightRange = m_clientState.window();
+ RectI tileRange = lightRange.padded(bufferTiles);
+
+ renderData.tileMinPosition = tileRange.min();
+ renderData.lightMinPosition = lightRange.min();
+
+ m_tileArray->tileEachTo(renderData.tiles, tileRange, [](RenderTile& renderTile, Vec2I const&, ClientTile const& clientTile) {
+ renderTile.foreground = clientTile.foreground;
+ renderTile.foregroundMod = clientTile.foregroundMod;
+
+ renderTile.background = clientTile.background;
+ renderTile.backgroundMod = clientTile.backgroundMod;
+
+ renderTile.foregroundHueShift = clientTile.foregroundHueShift;
+ renderTile.foregroundModHueShift = clientTile.foregroundModHueShift;
+ renderTile.foregroundColorVariant = clientTile.foregroundColorVariant;
+ renderTile.foregroundDamageType = clientTile.foregroundDamage.damageType();
+ renderTile.foregroundDamageLevel = floatToByte(clientTile.foregroundDamage.damageEffectPercentage());
+
+ renderTile.backgroundHueShift = clientTile.backgroundHueShift;
+ renderTile.backgroundModHueShift = clientTile.backgroundModHueShift;
+ renderTile.backgroundColorVariant = clientTile.backgroundColorVariant;
+ renderTile.backgroundDamageType = clientTile.backgroundDamage.damageType();
+ renderTile.backgroundDamageLevel = floatToByte(clientTile.backgroundDamage.damageEffectPercentage());
+
+ renderTile.liquidId = clientTile.liquid.liquid;
+ renderTile.liquidLevel = floatToByte(clientTile.liquid.level);
+ });
+
+ Vec2U lightSize(lightRange.size());
+
+ if (m_fullBright) {
+ renderData.lightMap.reset(lightSize, PixelFormat::RGB24);
+ renderData.lightMap.fill(Vec3B(255, 255, 255));
+ } else {
+ m_lightingCalculator.begin(lightRange);
+
+ Vec3F environmentLight = m_sky->environmentLight().toRgbF();
+ float undergroundLevel = m_worldTemplate->undergroundLevel();
+ auto liquidsDatabase = Root::singleton().liquidsDatabase();
+ auto materialDatabase = Root::singleton().materialDatabase();
+
+ // Each column in tileEvalColumns is guaranteed to be no larger than the sector size.
+ m_tileArray->tileEvalColumns(m_lightingCalculator.calculationRegion(), [&](Vec2I const& pos, ClientTile const* column, size_t ySize) {
+ size_t baseIndex = m_lightingCalculator.baseIndexFor(pos);
+ for (size_t y = 0; y < ySize; ++y) {
+ auto& tile = column[y];
+
+ Vec3F light = materialDatabase->radiantLight(tile.foreground, tile.foregroundMod);
+ light += liquidsDatabase->radiantLight(tile.liquid);
+ if (tile.foregroundLightTransparent) {
+ light += materialDatabase->radiantLight(tile.background, tile.backgroundMod);
+ if (tile.backgroundLightTransparent && pos[1] + y > undergroundLevel)
+ light += environmentLight;
+ }
+ m_lightingCalculator.setCellIndex(baseIndex + y, move(light), !tile.foregroundLightTransparent);
+ }
+ });
+
+ for (auto const& light : renderLightSources) {
+ Vec2F position = m_geometry.nearestTo(Vec2F(m_lightingCalculator.calculationRegion().min()), light.position);
+ if (light.pointLight)
+ m_lightingCalculator.addPointLight(position, Color::v3bToFloat(light.color), light.pointBeam, light.beamAngle, light.beamAmbience);
+ else
+ m_lightingCalculator.addSpreadLight(position, Color::v3bToFloat(light.color));
+ }
+
+ for (auto const& lightPair : m_particles->lightSources()) {
+ Vec2F position = m_geometry.nearestTo(Vec2F(m_lightingCalculator.calculationRegion().min()), lightPair.first);
+ m_lightingCalculator.addSpreadLight(position, Color::v3bToFloat(lightPair.second));
+ }
+
+ m_lightingCalculator.calculate(renderData.lightMap);
+ }
+
+ for (auto const& previewTile : previewTiles) {
+ Vec2I tileArrayPos = m_geometry.diff(previewTile.position, renderData.tileMinPosition);
+ if (tileArrayPos[0] >= 0 && tileArrayPos[0] < (int)renderData.tiles.size(0) && tileArrayPos[1] >= 0 && tileArrayPos[1] < (int)renderData.tiles.size(1)) {
+ RenderTile& renderTile = renderData.tiles(tileArrayPos[0], tileArrayPos[1]);
+
+ auto material = previewTile.matId;
+ auto hueShift = previewTile.hueShift;
+ auto colorVariant = previewTile.colorVariant;
+ if (previewTile.updateMatId) {
+ if (previewTile.foreground) {
+ renderTile.foreground = material;
+ renderTile.foregroundHueShift = hueShift;
+ renderTile.foregroundColorVariant = colorVariant;
+ } else {
+ renderTile.background = material;
+ renderTile.backgroundHueShift = hueShift;
+ renderTile.backgroundColorVariant = colorVariant;
+ }
+ }
+
+ if (previewTile.liqId != EmptyLiquidId) {
+ renderTile.liquidId = previewTile.liqId;
+ renderTile.liquidLevel = 1.0f;
+ }
+ }
+
+ if (previewTile.updateLight) {
+ Vec2I lightArrayPos = m_geometry.diff(previewTile.position, renderData.lightMinPosition);
+ if (lightArrayPos[0] >= 0 && lightArrayPos[0] < (int)renderData.lightMap.width() && lightArrayPos[1] >= 0 && lightArrayPos[1] < (int)renderData.lightMap.height())
+ renderData.lightMap.set(Vec2U(lightArrayPos), previewTile.light);
+ }
+ }
+
+ renderData.particles = m_particles->particles();
+ LogMap::set("active_particles", renderData.particles.size());
+
+ renderData.skyRenderData = m_sky->renderData();
+
+ auto environmentBiome = mainEnvironmentBiome();
+
+ m_parallaxFadeTimer.tick();
+ if (m_parallaxFadeTimer.ready() && m_nextParallax) {
+ m_currentParallax = m_nextParallax;
+ m_nextParallax.reset();
+ }
+
+ if (environmentBiome)
+ setParallax(environmentBiome->parallax);
+
+ if (m_currentParallax) {
+ if (m_parallaxFadeTimer.ready()) {
+ renderData.parallaxLayers.appendAll(m_currentParallax->layers());
+ } else {
+ for (auto layer : m_currentParallax->layers()) {
+ layer.alpha = min(1.0f, m_parallaxFadeTimer.percent() * 2);
+ renderData.parallaxLayers.append(layer);
+ }
+ }
+ }
+
+ if (m_nextParallax) {
+ for (auto layer : m_nextParallax->layers()) {
+ layer.alpha = min(1.0f, (1.0f - m_parallaxFadeTimer.percent()) * 2);
+ renderData.parallaxLayers.append(layer);
+ }
+ }
+
+ for (auto& layer : renderData.parallaxLayers) {
+ if (!layer.timeOfDayCorrelation.empty())
+ layer.alpha *= clamp((float)Root::singleton().functionDatabase()->function(layer.timeOfDayCorrelation)->evaluate(m_sky->timeOfDay() / m_sky->dayLength()), 0.0f, 1.0f);
+ }
+
+ stableSort(renderData.parallaxLayers, [](ParallaxLayer const& a, ParallaxLayer const& b) {
+ return tie(a.zLevel, a.verticalOrigin) > tie(b.zLevel, b.verticalOrigin);
+ });
+
+ auto overlayToDrawable = [](WorldStructure::Overlay const& overlay) -> Drawable {
+ Drawable drawable = Drawable::makeImage(overlay.image, 1.0f / TilePixels, false, overlay.min);
+ drawable.fullbright = overlay.fullbright;
+ return drawable;
+ };
+
+ renderData.backgroundOverlays = m_centralStructure.backgroundOverlays().transformed(overlayToDrawable);
+ renderData.foregroundOverlays = m_centralStructure.foregroundOverlays().transformed(overlayToDrawable);
+
+ renderData.isFullbright = m_fullBright;
+ renderData.dimLevel = m_worldDimLevel;
+ renderData.dimColor = m_worldDimColor;
+}
+
+List<AudioInstancePtr> WorldClient::pullPendingAudio() {
+ return take(m_samples);
+}
+
+List<AudioInstancePtr> WorldClient::pullPendingMusic() {
+ return take(m_music);
+}
+
+void WorldClient::dimWorld() {
+ m_worldDimTimer.reset();
+}
+
+bool WorldClient::interactiveHighlightMode() const {
+ return m_interactiveHighlightMode;
+}
+
+void WorldClient::setInteractiveHighlightMode(bool enabled) {
+ m_interactiveHighlightMode = enabled;
+}
+
+void WorldClient::setParallax(ParallaxPtr newParallax) {
+ if (newParallax) {
+ if (!m_currentParallax) {
+ m_currentParallax = newParallax;
+ } else if (m_parallaxFadeTimer.ready() && newParallax != m_currentParallax) {
+ m_nextParallax = newParallax;
+ m_parallaxFadeTimer.reset();
+ } else if (m_nextParallax && newParallax == m_currentParallax) {
+ m_currentParallax = m_nextParallax;
+ m_nextParallax = newParallax;
+ m_parallaxFadeTimer.invert();
+ }
+ }
+}
+
+void WorldClient::overrideGravity(float gravity) {
+ m_overrideGravity = gravity;
+}
+
+void WorldClient::resetGravity() {
+ m_overrideGravity = {};
+}
+
+bool WorldClient::toggleFullbright() {
+ m_fullBright = !m_fullBright;
+ return m_fullBright;
+}
+
+bool WorldClient::toggleCollisionDebug() {
+ m_collisionDebug = !m_collisionDebug;
+ return m_collisionDebug;
+}
+
+void WorldClient::handleIncomingPackets(List<PacketPtr> const& packets) {
+ auto& root = Root::singleton();
+ auto materialDatabase = root.materialDatabase();
+ auto itemDatabase = root.itemDatabase();
+ auto entityFactory = root.entityFactory();
+
+ for (auto const& packet : packets) {
+ if (!inWorld() && !is<WorldStartPacket>(packet))
+ Logger::error("WorldClient received packet type %s while not in world", PacketTypeNames.getRight(packet->type()));
+
+ if (auto worldStartPacket = as<WorldStartPacket>(packet)) {
+ initWorld(*worldStartPacket);
+
+ } else if (auto worldStopPacket = as<WorldStopPacket>(packet)) {
+ Logger::info("Client received world stop packet, leaving: %s", worldStopPacket->reason);
+ clearWorld();
+
+ } else if (auto entityCreate = as<EntityCreatePacket>(packet)) {
+ if (m_entityMap->entity(entityCreate->entityId)) {
+ Logger::error("WorldClient received entity create packet with duplicate entity id %s, deleting old entity.", entityCreate->entityId);
+ removeEntity(entityCreate->entityId, false);
+ }
+
+ auto entity = entityFactory->netLoadEntity(entityCreate->entityType, entityCreate->storeData);
+ entity->readNetState(entityCreate->firstNetState);
+ entity->init(this, entityCreate->entityId, EntityMode::Slave);
+ m_entityMap->addEntity(entity);
+
+ if (m_interpolationTracker.interpolationEnabled()) {
+ entity->enableInterpolation(m_interpolationTracker.extrapolationHint());
+
+ // Delay appearance of new slaved entities to match with interplation
+ // state.
+ m_startupHiddenEntities.add(entityCreate->entityId);
+ timer(round(m_interpolationTracker.interpolationLeadSteps()), [this, entityId = entityCreate->entityId](World*) {
+ m_startupHiddenEntities.remove(entityId);
+ });
+ }
+
+ } else if (auto entityUpdateSet = as<EntityUpdateSetPacket>(packet)) {
+ float interpolationLeadTime = m_interpolationTracker.interpolationLeadSteps() * WorldTimestep;
+ m_entityMap->forAllEntities([&](EntityPtr const& entity) {
+ EntityId entityId = entity->entityId();
+ if (connectionForEntity(entityId) == entityUpdateSet->forConnection) {
+ starAssert(entity->isSlave());
+ entity->readNetState(entityUpdateSet->deltas.value(entityId), interpolationLeadTime);
+ }
+ });
+
+ } else if (auto entityDestroy = as<EntityDestroyPacket>(packet)) {
+ if (auto entity = m_entityMap->entity(entityDestroy->entityId)) {
+ entity->readNetState(entityDestroy->finalNetState, m_interpolationTracker.interpolationLeadSteps() * WorldTimestep);
+
+ // Before destroying the entity, we should make sure that the entity is
+ // using the absolute latest data, so we disable interpolation.
+
+ if (m_interpolationTracker.interpolationEnabled() && entityDestroy->death) {
+ // Delay death packets by the interpolation step to give time for
+ // interpolation to catch up.
+ timer(round(m_interpolationTracker.interpolationLeadSteps()), [this, entity, entityDestroy](World*) {
+ entity->disableInterpolation();
+ removeEntity(entityDestroy->entityId, entityDestroy->death);
+ });
+ } else {
+ entity->disableInterpolation();
+ removeEntity(entityDestroy->entityId, entityDestroy->death);
+ }
+ }
+
+ } else if (auto structurePacket = as<CentralStructureUpdatePacket>(packet)) {
+ m_centralStructure = WorldStructure(structurePacket->structureData);
+
+ } else if (auto tileArrayUpdate = as<TileArrayUpdatePacket>(packet)) {
+ RectI tileRegion = RectI::withSize(tileArrayUpdate->min, Vec2I(tileArrayUpdate->array.size()));
+
+ // NOTE: We're creating client side sectors on tileArrayUpdate here, and
+ // at no other time, and this is sort of a big assumption that
+ // tileArrayUpdate happens for all valid client side sectors first before
+ // any other tile updates.
+ for (auto const& sector : m_tileArray->validSectorsFor(tileRegion))
+ m_tileArray->loadDefaultSector(sector);
+
+ for (int x = tileRegion.xMin(); x < tileRegion.xMax(); ++x) {
+ for (int y = tileRegion.yMin(); y < tileRegion.yMax(); ++y)
+ readNetTile({x, y}, tileArrayUpdate->array(x - tileRegion.xMin(), y - tileRegion.yMin()));
+ }
+
+ } else if (auto tileUpdate = as<TileUpdatePacket>(packet)) {
+ readNetTile(tileUpdate->position, tileUpdate->tile);
+
+ } else if (auto tileDamageUpdate = as<TileDamageUpdatePacket>(packet)) {
+ if (ClientTile* tile = m_tileArray->modifyTile(tileDamageUpdate->position)) {
+ if (tileDamageUpdate->layer == TileLayer::Foreground)
+ tile->foregroundDamage = tileDamageUpdate->tileDamage;
+ else
+ tile->backgroundDamage = tileDamageUpdate->tileDamage;
+
+ m_damagedBlocks.add(tileDamageUpdate->position);
+ }
+
+ } else if (auto tileModificationFailure = as<TileModificationFailurePacket>(packet)) {
+ // TODO: Right now we assume that every tile modification was caused by a
+ // player, but this may not be true in the future. In the future, there
+ // may be context hints with tile modifications to figure out what to do
+ // with failures.
+ for (auto modification : tileModificationFailure->modifications) {
+ m_predictedTiles.remove(modification.first);
+ if (auto placeMaterial = modification.second.ptr<PlaceMaterial>()) {
+ auto stack = materialDatabase->materialItemDrop(placeMaterial->material);
+ tryGiveMainPlayerItem(itemDatabase->item(stack));
+ } else if (auto placeMod = modification.second.ptr<PlaceMod>()) {
+ auto stack = materialDatabase->modItemDrop(placeMod->mod);
+ tryGiveMainPlayerItem(itemDatabase->item(stack));
+ }
+ }
+
+ } else if (auto liquidUpdate = as<TileLiquidUpdatePacket>(packet)) {
+ m_predictedTiles.remove(liquidUpdate->position);
+ if (ClientTile* tile = m_tileArray->modifyTile(liquidUpdate->position))
+ tile->liquid = liquidUpdate->liquidUpdate.liquidLevel();
+
+ } else if (auto giveItem = as<GiveItemPacket>(packet)) {
+ tryGiveMainPlayerItem(itemDatabase->item(giveItem->item));
+
+ } else if (auto stepUpdate = as<StepUpdatePacket>(packet)) {
+ m_interpolationTracker.receiveStepUpdate(stepUpdate->remoteStep);
+
+ } else if (auto environmentUpdatePacket = as<EnvironmentUpdatePacket>(packet)) {
+ m_sky->readUpdate(environmentUpdatePacket->skyDelta);
+ m_weather.readUpdate(environmentUpdatePacket->weatherDelta);
+
+ } else if (auto hit = as<HitRequestPacket>(packet)) {
+ m_damageManager->pushRemoteHitRequest(hit->remoteHitRequest);
+
+ } else if (auto damage = as<DamageRequestPacket>(packet)) {
+ m_damageManager->pushRemoteDamageRequest(damage->remoteDamageRequest);
+
+ } else if (auto damage = as<DamageNotificationPacket>(packet)) {
+ m_damageManager->pushRemoteDamageNotification(damage->remoteDamageNotification);
+
+ } else if (auto entityMessagePacket = as<EntityMessagePacket>(packet)) {
+ EntityPtr entity;
+ if (entityMessagePacket->entityId.is<EntityId>())
+ entity = m_entityMap->entity(entityMessagePacket->entityId.get<EntityId>());
+ else
+ entity = m_entityMap->uniqueEntity(entityMessagePacket->entityId.get<String>());
+
+ if (!entity) {
+ m_outgoingPackets.append(make_shared<EntityMessageResponsePacket>(makeLeft("Unknown entity"), entityMessagePacket->uuid));
+
+ } else if (!entity->isMaster()) {
+ Logger::error("Server has sent a scripted entity response for a slave entity");
+ m_outgoingPackets.append(make_shared<EntityMessageResponsePacket>(makeLeft("Entity delivery error"), entityMessagePacket->uuid));
+
+ } else {
+ auto response = entity->receiveMessage(entityMessagePacket->fromConnection, entityMessagePacket->message, entityMessagePacket->args);
+ if (response)
+ m_outgoingPackets.append(make_shared<EntityMessageResponsePacket>(makeRight(response.take()), entityMessagePacket->uuid));
+ else
+ m_outgoingPackets.append(make_shared<EntityMessageResponsePacket>(makeLeft("Message not handled by entity"), entityMessagePacket->uuid));
+ }
+
+ } else if (auto entityMessageResponsePacket = as<EntityMessageResponsePacket>(packet)) {
+ if (!m_entityMessageResponses.contains(entityMessageResponsePacket->uuid))
+ throw WorldClientException("EntityMessageResponse received for unknown context!");
+
+ auto response = m_entityMessageResponses.take(entityMessageResponsePacket->uuid);
+ if (entityMessageResponsePacket->response.isRight())
+ response.fulfill(entityMessageResponsePacket->response.right());
+ else
+ response.fail(entityMessageResponsePacket->response.left());
+
+ } else if (auto updateWorldProperties = as<UpdateWorldPropertiesPacket>(packet)) {
+ m_worldProperties.merge(updateWorldProperties->updatedProperties, true);
+
+ } else if (auto updateTileProtection = as<UpdateTileProtectionPacket>(packet)) {
+ setTileProtection(updateTileProtection->dungeonId, updateTileProtection->isProtected);
+
+ } else if (auto setDungeonGravity = as<SetDungeonGravityPacket>(packet)) {
+ if (setDungeonGravity->gravity)
+ m_dungeonIdGravity[setDungeonGravity->dungeonId] = *setDungeonGravity->gravity;
+ else
+ m_dungeonIdGravity.remove(setDungeonGravity->dungeonId);
+
+ } else if (auto setDungeonBreathable = as<SetDungeonBreathablePacket>(packet)) {
+ if (setDungeonBreathable->breathable.isValid())
+ m_dungeonIdBreathable[setDungeonBreathable->dungeonId] = *setDungeonBreathable->breathable;
+ else
+ m_dungeonIdBreathable.remove(setDungeonBreathable->dungeonId);
+
+ } else if (auto entityInteract = as<EntityInteractPacket>(packet)) {
+ auto interactResult = interact(entityInteract->interactRequest).result();
+ m_outgoingPackets.append(make_shared<EntityInteractResultPacket>(interactResult.take(), entityInteract->requestId, entityInteract->interactRequest.sourceId));
+
+ } else if (auto interactResult = as<EntityInteractResultPacket>(packet)) {
+ auto response = m_entityInteractionResponses.take(interactResult->requestId);
+ if (interactResult->action)
+ response.fulfill(interactResult->action);
+ else
+ response.fail("no interaction result");
+
+ } else if (auto setPlayerStart = as<SetPlayerStartPacket>(packet)) {
+ m_playerStart = setPlayerStart->playerStart;
+ m_respawnInWorld = setPlayerStart->respawnInWorld;
+
+ } else if (auto findUniqueEntityResponse = as<FindUniqueEntityResponsePacket>(packet)) {
+ for (auto& promise : take(m_findUniqueEntityResponses[findUniqueEntityResponse->uniqueEntityId])) {
+ if (findUniqueEntityResponse->entityPosition)
+ promise.fulfill(*findUniqueEntityResponse->entityPosition);
+ else
+ promise.fail("Unknown entity");
+ }
+
+ } else if (auto worldLayoutUpdate = as<WorldLayoutUpdatePacket>(packet)) {
+ m_worldTemplate->setWorldLayout(make_shared<WorldLayout>(worldLayoutUpdate->layoutData));
+
+ } else if (auto worldParametersUpdate = as<WorldParametersUpdatePacket>(packet)) {
+ m_worldTemplate->setWorldParameters(netLoadVisitableWorldParameters(worldParametersUpdate->parametersData));
+
+ } else if (auto pongPacket = as<PongPacket>(packet)) {
+ if (m_pingTime)
+ m_latency = Time::monotonicMilliseconds() - m_pingTime.take();
+
+ } else {
+ Logger::error("Improper packet type %s received by client", (int)packet->type());
+ }
+ }
+}
+
+List<PacketPtr> WorldClient::getOutgoingPackets() {
+ return std::move(m_outgoingPackets);
+}
+
+void WorldClient::update() {
+ if (!inWorld())
+ return;
+
+ auto assets = Root::singleton().assets();
+
+ m_lightingCalculator.setMonochrome(Root::singleton().configuration()->get("monochromeLighting").toBool());
+
+ auto predictedTilesIt = makeSMutableMapIterator(m_predictedTiles);
+ while (predictedTilesIt.hasNext()) {
+ auto& entry = predictedTilesIt.next();
+ if (entry.second++ > m_modifiedTilePredictionTimeout) {
+ predictedTilesIt.remove();
+ }
+ }
+
+ ++m_currentStep;
+ m_interpolationTracker.update(m_currentStep);
+
+ List<WorldAction> triggeredActions;
+ eraseWhere(m_timers, [&triggeredActions](pair<int, WorldAction>& timer) {
+ if (--timer.first <= 0) {
+ triggeredActions.append(timer.second);
+ return true;
+ }
+ return false;
+ });
+ for (auto const& action : triggeredActions)
+ action(this);
+
+ List<EntityId> toRemove;
+ List<EntityId> clientPresenceEntities{m_mainPlayer->entityId()};
+ m_entityMap->updateAllEntities([&](EntityPtr const& entity) {
+ entity->update(m_currentStep);
+
+ if (entity->shouldDestroy() && entity->entityMode() == EntityMode::Master)
+ toRemove.append(entity->entityId());
+ if (entity->isMaster() && entity->clientEntityMode() == ClientEntityMode::ClientPresenceMaster)
+ clientPresenceEntities.append(entity->entityId());
+ }, [](EntityPtr const& a, EntityPtr const& b) {
+ return a->entityType() < b->entityType();
+ });
+
+ m_clientState.setPlayer(m_mainPlayer->entityId());
+ m_clientState.setClientPresenceEntities(move(clientPresenceEntities));
+
+ m_damageManager->update();
+ handleDamageNotifications();
+
+ m_sky->setAltitude(m_clientState.windowCenter()[1]);
+ m_sky->update();
+
+ RectI particleRegion = m_clientState.window().padded(m_clientConfig.getInt("particleRegionPadding"));
+
+ m_weather.setVisibleRegion(particleRegion);
+ m_weather.update();
+
+ if (!m_mainPlayer->isDead()) {
+ // Clear m_requestedDrops every so often in case of entity id reuse or
+ // desyncs etc
+ if (m_currentStep % m_clientConfig.getInt("itemRequestReset") == 0)
+ m_requestedDrops.clear();
+
+ Vec2F playerPos = m_mainPlayer->position();
+ auto dropList = m_entityMap->query<ItemDrop>(RectF(playerPos - Vec2F::filled(DropDist / 2), playerPos + Vec2F::filled(DropDist / 2)));
+ for (auto itemDrop : dropList) {
+ auto distSquared = m_geometry.diff(itemDrop->position(), playerPos).magnitudeSquared();
+
+ // If the drop is within DropDist and not owned, request it.
+ if (itemDrop->canTake() && !m_requestedDrops.contains(itemDrop->entityId()) && distSquared < square(DropDist)) {
+ m_requestedDrops.add(itemDrop->entityId());
+ if (m_mainPlayer->itemsCanHold(itemDrop->item()) != 0)
+ m_outgoingPackets.append(make_shared<RequestDropPacket>(itemDrop->entityId()));
+ }
+ }
+ } else {
+ m_requestedDrops.clear();
+ }
+
+ sparkDamagedBlocks();
+
+ m_particles->addParticles(m_weather.pullNewParticles());
+ m_particles->update(WorldTimestep, RectF(particleRegion), m_weather.wind());
+
+ if (auto audioSample = m_ambientSounds.updateAmbient(currentAmbientNoises(), m_sky->isDayTime()))
+ m_samples.append(audioSample);
+ if (auto audioSample = m_ambientSounds.updateWeather(currentWeatherNoises()))
+ m_samples.append(audioSample);
+
+ if (inSpace()) {
+ m_samples.appendAll(m_sky->pullSounds());
+
+ if (m_spaceSound && m_spaceSound->finished()) {
+ m_spaceSound = {};
+ m_activeSpaceSound = "";
+ }
+
+ auto skyAmbientNoise = m_sky->ambientNoise();
+ if (skyAmbientNoise != m_activeSpaceSound) {
+ if (m_spaceSound) {
+ m_spaceSound->stop(skyAmbientNoise == "" ? 3.0 : 0.0);
+ } else {
+ m_activeSpaceSound = skyAmbientNoise;
+ if (!m_activeSpaceSound.empty()) {
+ m_spaceSound = make_shared<AudioInstance>(*assets->audio(m_activeSpaceSound));
+ m_samples.append(m_spaceSound);
+ }
+ }
+ }
+ }
+
+ if (auto newAltMusic = m_mainPlayer->pullPendingAltMusic()) {
+ if (newAltMusic->first)
+ playAltMusic(newAltMusic->first.get(), newAltMusic->second);
+ else
+ stopAltMusic(newAltMusic->second);
+ }
+
+ if (auto audioSample = m_altMusicTrack.updateAmbient(currentAltMusicTrack(), true))
+ m_music.append(audioSample);
+
+ if (auto audioSample = m_musicTrack.updateAmbient(currentMusicTrack(), m_sky->isDayTime()))
+ m_music.append(audioSample);
+
+ for (EntityId entityId : toRemove)
+ removeEntity(entityId, true);
+
+ queueUpdatePackets();
+
+ if (m_pingTime.isNothing()) {
+ m_pingTime = Time::monotonicMilliseconds();
+ m_outgoingPackets.append(make_shared<PingPacket>());
+ }
+ LogMap::set("client_ping", m_latency);
+
+ // Remove active sectors that are outside of the current monitoring region
+ Set<ClientTileSectorArray::Sector> neededSectors;
+ auto monitoredRegions = m_clientState.monitoringRegions([this](EntityId entityId) -> Maybe<RectI> {
+ if (auto entity = this->entity(entityId))
+ return RectI::integral(entity->metaBoundBox().translated(entity->position()));
+ return {};
+ });
+ for (auto monitoredRegion : monitoredRegions)
+ neededSectors.addAll(m_tileArray->validSectorsFor(monitoredRegion.padded(WorldSectorSize)));
+
+ auto loadedSectors = m_tileArray->loadedSectors();
+ for (auto sector : loadedSectors) {
+ if (!neededSectors.contains(sector))
+ m_tileArray->unloadSector(sector);
+ }
+
+ if (m_collisionDebug)
+ renderCollisionDebug();
+
+ LogMap::set("client_entities", m_entityMap->size());
+ LogMap::set("client_sectors", strf("%d", loadedSectors.size()));
+ LogMap::set("client_lua_mem", m_luaRoot->luaMemoryUsage());
+}
+
+ConnectionId WorldClient::connection() const {
+ return *m_clientId;
+}
+
+WorldGeometry WorldClient::geometry() const {
+ return m_geometry;
+}
+
+uint64_t WorldClient::currentStep() const {
+ return m_currentStep;
+}
+
+MaterialId WorldClient::material(Vec2I const& pos, TileLayer layer) const {
+ if (!inWorld())
+ return NullMaterialId;
+ return m_tileArray->tile(pos).material(layer);
+}
+
+MaterialHue WorldClient::materialHueShift(Vec2I const& position, TileLayer layer) const {
+ if (!inWorld())
+ return MaterialHue();
+ auto const& tile = m_tileArray->tile(position);
+ return layer == TileLayer::Foreground ? tile.foregroundHueShift : tile.backgroundHueShift;
+}
+
+ModId WorldClient::mod(Vec2I const& pos, TileLayer layer) const {
+ if (!inWorld())
+ return NoModId;
+ return m_tileArray->tile(pos).mod(layer);
+}
+
+MaterialHue WorldClient::modHueShift(Vec2I const& position, TileLayer layer) const {
+ if (!inWorld())
+ return MaterialHue();
+ auto const& tile = m_tileArray->tile(position);
+ return layer == TileLayer::Foreground ? tile.foregroundModHueShift : tile.backgroundModHueShift;
+}
+
+MaterialColorVariant WorldClient::colorVariant(Vec2I const& position, TileLayer layer) const {
+ if (!inWorld())
+ return MaterialColorVariant();
+ auto const& tile = m_tileArray->tile(position);
+ return layer == TileLayer::Foreground ? tile.foregroundColorVariant : tile.backgroundColorVariant;
+}
+
+EntityPtr WorldClient::entity(EntityId entityId) const {
+ if (!inWorld())
+ return {};
+
+ return m_entityMap->entity(entityId);
+}
+
+void WorldClient::addEntity(EntityPtr const& entity) {
+ if (!entity)
+ return;
+
+ if (!inWorld())
+ return;
+
+ if (entity->clientEntityMode() != ClientEntityMode::ClientSlaveOnly) {
+ entity->init(this, m_entityMap->reserveEntityId(), EntityMode::Master);
+ m_entityMap->addEntity(entity);
+ } else {
+ auto entityFactory = Root::singleton().entityFactory();
+ m_outgoingPackets.append(make_shared<SpawnEntityPacket>(entity->entityType(), entityFactory->netStoreEntity(entity), entity->writeNetState().first));
+ }
+}
+
+TileDamageResult WorldClient::damageTiles(List<Vec2I> const& pos, TileLayer layer, Vec2F const& sourcePosition, TileDamage const& tileDamage, Maybe<EntityId> sourceEntity) {
+ if (!inWorld())
+ return TileDamageResult::None;
+
+ // Filter out any tiles that are not currently occupied or are protected
+ auto occupied = pos.filtered([this, layer](Vec2I pos) { return tileIsOccupied(pos, layer, true); });
+ auto toDamage = occupied.filtered([this](Vec2I pos) { return !isTileProtected(pos); });
+ auto toDing = occupied.filtered([this](Vec2I pos) { return isTileProtected(pos); });
+
+ if (toDamage.size() + toDing.size() == 0)
+ return TileDamageResult::None;
+
+ auto res = TileDamageResult::None;
+
+ if (toDing.size()) {
+ auto dingDamage = tileDamage;
+ dingDamage.type = TileDamageType::Protected;
+ m_outgoingPackets.append(make_shared<DamageTileGroupPacket>(move(toDing), layer, sourcePosition, dingDamage, Maybe<EntityId>()));
+ res = TileDamageResult::Protected;
+ }
+
+ if (toDamage.size()) {
+ m_outgoingPackets.append(make_shared<DamageTileGroupPacket>(move(toDamage), layer, sourcePosition, tileDamage, sourceEntity));
+ res = TileDamageResult::Normal;
+ }
+
+ return res;
+}
+
+DungeonId WorldClient::dungeonId(Vec2I const& pos) const {
+ if (!inWorld())
+ return NoDungeonId;
+
+ return m_tileArray->tile(pos).dungeonId;
+}
+
+void WorldClient::collectLiquid(List<Vec2I> const& tilePositions, LiquidId liquidId) {
+ if (!inWorld())
+ return;
+
+ m_outgoingPackets.append(make_shared<CollectLiquidPacket>(tilePositions, liquidId));
+}
+
+bool WorldClient::isTileProtected(Vec2I const& pos) const {
+ if (!inWorld())
+ return true;
+
+ auto tile = m_tileArray->tile(pos);
+ return m_protectedDungeonIds.contains(tile.dungeonId);
+}
+
+void WorldClient::setTileProtection(DungeonId dungeonId, bool isProtected) {
+ if (isProtected) {
+ m_protectedDungeonIds.add(dungeonId);
+ } else {
+ m_protectedDungeonIds.remove(dungeonId);
+ }
+}
+
+void WorldClient::queueUpdatePackets() {
+ auto& root = Root::singleton();
+ auto assets = root.assets();
+ auto entityFactory = root.entityFactory();
+
+ m_outgoingPackets.append(make_shared<StepUpdatePacket>(m_currentStep));
+
+ if (m_currentStep % m_clientConfig.getInt("worldClientStateUpdateDelta") == 0)
+ m_outgoingPackets.append(make_shared<WorldClientStateUpdatePacket>(m_clientState.writeDelta()));
+
+ m_entityMap->forAllEntities([&](EntityPtr const& entity) {
+ if (entity->isMaster() && !m_masterEntitiesNetVersion.contains(entity->entityId())) {
+ // Server was unaware of this entity until now
+ auto firstNetState = entity->writeNetState();
+ m_masterEntitiesNetVersion[entity->entityId()] = firstNetState.second;
+ m_outgoingPackets.append(make_shared<EntityCreatePacket>(entity->entityType(),
+ entityFactory->netStoreEntity(entity), move(firstNetState.first), entity->entityId()));
+ }
+ });
+
+ if (m_currentStep % m_interpolationTracker.entityUpdateDelta() == 0) {
+ auto entityUpdateSet = make_shared<EntityUpdateSetPacket>();
+ entityUpdateSet->forConnection = *m_clientId;
+ m_entityMap->forAllEntities([&](EntityPtr const& entity) {
+ if (auto version = m_masterEntitiesNetVersion.ptr(entity->entityId())) {
+ auto updateAndVersion = entity->writeNetState(*version);
+ if (!updateAndVersion.first.empty())
+ entityUpdateSet->deltas[entity->entityId()] = move(updateAndVersion.first);
+ *version = updateAndVersion.second;
+ }
+ });
+ m_outgoingPackets.append(move(entityUpdateSet));
+ }
+
+ for (auto& remoteHitRequest : m_damageManager->pullRemoteHitRequests())
+ m_outgoingPackets.append(make_shared<HitRequestPacket>(move(remoteHitRequest)));
+ for (auto& remoteDamageRequest : m_damageManager->pullRemoteDamageRequests())
+ m_outgoingPackets.append(make_shared<DamageRequestPacket>(move(remoteDamageRequest)));
+ for (auto& remoteDamageNotification : m_damageManager->pullRemoteDamageNotifications())
+ m_outgoingPackets.append(make_shared<DamageNotificationPacket>(move(remoteDamageNotification)));
+}
+
+void WorldClient::handleDamageNotifications() {
+ if (!inWorld())
+ return;
+
+ auto renderParticle = [&](Vec2F position, float amount, String const& damageNumberParticleKind) {
+ int displayValue = (int)ceil(amount - 0.1f);
+ if (displayValue <= 0)
+ return;
+ Particle particle = Root::singleton().particleDatabase()->particle(damageNumberParticleKind);
+ particle.position += position;
+ particle.string = particle.string.replace("$dmg$", strf("%s", displayValue));
+ m_particles->add(particle);
+ };
+
+ eraseWhere(m_damageNumbers, [&](std::pair<DamageNumberKey, DamageNumber> const& entry) -> bool {
+ if (Time::monotonicTime() - entry.second.timestamp > m_damageNotificationBatchDuration) {
+ renderParticle(entry.second.position, entry.second.amount, entry.first.damageNumberParticleKind);
+ return true;
+ }
+ return false;
+ });
+
+ for (auto const& damageNotification : m_damageManager->pullPendingNotifications()) {
+ auto damageDatabase = Root::singleton().damageDatabase();
+ DamageKind const& damageKind = damageDatabase->damageKind(damageNotification.damageSourceKind);
+ ElementalType const& elementalType = damageDatabase->elementalType(damageKind.elementalType);
+
+ auto damageNumberParticleKind = elementalType.damageNumberParticles.get(damageNotification.hitType);
+ auto damageNumberKey = DamageNumberKey{ damageNumberParticleKind, damageNotification.sourceEntityId, damageNotification.targetEntityId};
+
+
+ DamageNumber number;
+ if (m_damageNumbers.contains(damageNumberKey)) {
+ number = m_damageNumbers.take(damageNumberKey);
+
+ if (damageNotification.hitType == HitType::Kill)
+ renderParticle(damageNotification.position,
+ damageNotification.damageDealt + number.amount,
+ damageNumberKey.damageNumberParticleKind);
+ } else {
+ if (damageNotification.hitType == HitType::Kill)
+ renderParticle(damageNotification.position, damageNotification.damageDealt, damageNumberParticleKind);
+ number.amount = 0;
+ number.timestamp = Time::monotonicTime();
+ }
+
+ if (damageNotification.hitType != HitType::Kill) {
+ number.position = damageNotification.position;
+ number.amount += damageNotification.damageDealt;
+ m_damageNumbers[damageNumberKey] = number;
+ }
+
+ String material = damageNotification.targetMaterialKind;
+ if (!material.empty() && damageKind.effects.contains(material)) {
+ // default to normal hit
+ HitType effectHitType = damageKind.effects.get(material).contains(damageNotification.hitType) ? damageNotification.hitType : HitType::Hit;
+ m_samples.appendAll(soundsFromDefinition(damageKind.effects.get(material).get(effectHitType).sounds, damageNotification.position));
+
+ auto hitParticles = particlesFromDefinition(damageKind.effects.get(material).get(effectHitType).particles, damageNotification.position);
+
+ Maybe<StringList> directives;
+ if (auto worldTemplate = m_worldTemplate) {
+ if(auto parameters = worldTemplate->worldParameters())
+ directives = m_worldTemplate->worldParameters()->globalDirectives;
+ }
+ if (directives) {
+ int directiveIndex = unsigned(damageNotification.targetEntityId) % directives->size();
+ for (auto& p : hitParticles)
+ p.directives = directives->get(directiveIndex);
+ }
+
+ m_particles->addParticles(hitParticles);
+ }
+ }
+}
+
+void WorldClient::sparkDamagedBlocks() {
+ if (!inWorld())
+ return;
+
+ auto materialDatabase = Root::singleton().materialDatabase();
+
+ for (auto pos : m_damagedBlocks.values()) {
+ if (auto tile = m_tileArray->modifyTile(pos)) {
+ if (tile->backgroundDamage.healthy() && tile->foregroundDamage.healthy())
+ m_damagedBlocks.remove(pos);
+
+ if (isRealMaterial(tile->foreground) && tile->foregroundDamage.damageEffectPercentage() - Random::randf() > 0.0f
+ && (Random::randf() < m_blockDamageParticleProbability)) {
+ auto particle = m_blockDamageParticle;
+ particle.color = materialDatabase->materialParticleColor(tile->foreground, tile->foregroundHueShift);
+
+ if (isTileProtected(pos))
+ particle = m_blockDingParticle;
+
+ particle.position += centerOfTile(pos);
+ particle.velocity = particle.velocity.magnitude()
+ * vnorm(m_geometry.diff(tile->foregroundDamage.sourcePosition(), particle.position));
+ particle.applyVariance(m_blockDamageParticleVariance);
+ m_particles->add(particle);
+ }
+
+ if (isRealMaterial(tile->background) && tile->backgroundDamage.damageEffectPercentage() - Random::randf() > 0.0f
+ && (Random::randf() < m_blockDamageParticleProbability)) {
+ auto particle = m_blockDamageParticle;
+ particle.color = materialDatabase->materialParticleColor(tile->background, tile->backgroundHueShift);
+
+ if (isTileProtected(pos))
+ particle = m_blockDingParticle;
+
+ particle.position += centerOfTile(pos);
+ particle.velocity = particle.velocity.magnitude()
+ * vnorm(m_geometry.diff(tile->backgroundDamage.sourcePosition(), particle.position));
+ particle.applyVariance(m_blockDamageParticleVariance);
+ m_particles->add(particle);
+ }
+ }
+ }
+}
+
+void WorldClient::removeEntity(EntityId entityId, bool andDie) {
+ auto entity = m_entityMap->entity(entityId);
+ if (!entity)
+ return;
+
+ if (andDie) {
+ ClientRenderCallback renderCallback;
+ entity->destroy(&renderCallback);
+
+ Maybe<StringList> directives;
+ if (auto worldTemplate = m_worldTemplate) {
+ if(auto parameters = worldTemplate->worldParameters())
+ directives = m_worldTemplate->worldParameters()->globalDirectives;
+ }
+ if (directives) {
+ int directiveIndex = unsigned(entity->entityId()) % directives->size();
+ for (auto& p : renderCallback.particles)
+ p.directives = directives->get(directiveIndex);
+ }
+
+ m_particles->addParticles(move(renderCallback.particles));
+ m_samples.appendAll(move(renderCallback.audios));
+ }
+
+ if (auto version = m_masterEntitiesNetVersion.maybeTake(entity->entityId())) {
+ ByteArray finalNetState = entity->writeNetState(*version).first;
+ m_outgoingPackets.append(make_shared<EntityDestroyPacket>(entity->entityId(), move(finalNetState), andDie));
+ }
+
+ m_entityMap->removeEntity(entityId);
+ entity->uninit();
+}
+
+InteractiveEntityPtr WorldClient::getInteractiveInRange(Vec2F const& targetPosition, Vec2F const& sourcePosition, float maxRange) const {
+ if (!inWorld())
+ return {};
+ return WorldImpl::getInteractiveInRange(m_geometry, m_entityMap, targetPosition, sourcePosition, maxRange);
+}
+
+bool WorldClient::canReachEntity(Vec2F const& position, float radius, EntityId targetEntity, bool preferInteractive) const {
+ if (!inWorld())
+ return false;
+ return WorldImpl::canReachEntity(m_geometry, m_tileArray, m_entityMap, position, radius, targetEntity, preferInteractive);
+}
+
+RpcPromise<InteractAction> WorldClient::interact(InteractRequest const& request) {
+ if (!inWorld())
+ return RpcPromise<InteractAction>::createFailed("not initialized in world");
+
+ if (auto targetEntity = m_entityMap->entity(request.targetId)) {
+ if (targetEntity->isMaster()) {
+ // client-side-master entities need to be handled here rather than over network
+ auto interactiveTarget = as<InteractiveEntity>(targetEntity);
+ starAssert(interactiveTarget);
+
+ return RpcPromise<InteractAction>::createFulfilled(interactiveTarget->interact(request));
+ }
+ }
+
+ auto pair = RpcPromise<InteractAction>::createPair();
+ Uuid requestId;
+ m_entityInteractionResponses[requestId] = pair.second;
+ m_outgoingPackets.append(make_shared<EntityInteractPacket>(request, requestId));
+
+ return pair.first;
+}
+
+void WorldClient::initWorld(WorldStartPacket const& startPacket) {
+ clearWorld();
+ m_outgoingPackets.append(make_shared<WorldStartAcknowledgePacket>());
+
+ auto assets = Root::singleton().assets();
+ if (startPacket.localInterpolationMode)
+ m_interpolationTracker = InterpolationTracker(m_clientConfig.query("interpolationSettings.local"));
+ else
+ m_interpolationTracker = InterpolationTracker(m_clientConfig.query("interpolationSettings.normal"));
+
+ m_clientId = startPacket.clientId;
+ auto entitySpace = connectionEntitySpace(startPacket.clientId);
+ m_worldTemplate = make_shared<WorldTemplate>(startPacket.templateData);
+ m_entityMap = make_shared<EntityMap>(m_worldTemplate->size(), entitySpace.first, entitySpace.second);
+ m_tileArray = make_shared<ClientTileSectorArray>(m_worldTemplate->size());
+ m_damageManager = make_shared<DamageManager>(this, startPacket.clientId);
+ m_luaRoot = make_shared<LuaRoot>();
+ m_luaRoot->tuneAutoGarbageCollection(m_clientConfig.getFloat("luaGcPause"), m_clientConfig.getFloat("luaGcStepMultiplier"));
+ m_playerStart = startPacket.playerRespawn;
+ m_respawnInWorld = startPacket.respawnInWorld;
+ m_worldProperties = startPacket.worldProperties.optObject().value();
+ m_dungeonIdGravity = startPacket.dungeonIdGravity;
+ m_dungeonIdBreathable = startPacket.dungeonIdBreathable;
+ m_protectedDungeonIds = startPacket.protectedDungeonIds;
+
+ m_geometry = WorldGeometry(m_worldTemplate->size());
+
+ m_particles = make_shared<ParticleManager>(m_geometry, m_tileArray);
+ m_particles->setUndergroundLevel(m_worldTemplate->undergroundLevel());
+
+ setupForceRegions();
+
+ if (!m_mainPlayer->isDead()) {
+ m_mainPlayer->init(this, m_entityMap->reserveEntityId(), EntityMode::Master);
+ m_entityMap->addEntity(m_mainPlayer);
+ }
+ m_mainPlayer->moveTo(startPacket.playerStart);
+ if (m_worldTemplate->worldParameters())
+ m_mainPlayer->overrideTech(m_worldTemplate->worldParameters()->overrideTech);
+ else
+ m_mainPlayer->overrideTech({});
+
+ // Auto reposition the client window on the player when the main player
+ // changes position.
+ centerClientWindowOnPlayer();
+
+ m_sky = make_shared<Sky>();
+ m_sky->readUpdate(startPacket.skyData);
+
+ m_weather.setup(m_geometry, [this](Vec2I const& pos) {
+ auto const& tile = m_tileArray->tile(pos);
+ return !isRealMaterial(tile.background) && !isSolidColliding(tile.collision);
+ });
+ m_weather.readUpdate(startPacket.weatherData);
+
+ m_lightingCalculator.setMonochrome(Root::singleton().configuration()->get("monochromeLighting").toBool());
+ m_lightingCalculator.setParameters(assets->json("/lighting.config:lighting"));
+ m_lightIntensityCalculator.setParameters(assets->json("/lighting.config:intensity"));
+
+ m_inWorld = true;
+}
+
+void WorldClient::clearWorld() {
+ if (m_entityMap) {
+ while (m_entityMap->size() > 0) {
+ for (auto entityId : m_entityMap->entityIds())
+ removeEntity(entityId, false);
+ }
+ }
+
+ m_currentStep = 0;
+ m_inWorld = false;
+ m_clientId.reset();
+
+ m_interpolationTracker = InterpolationTracker();
+
+ m_masterEntitiesNetVersion.clear();
+ m_outgoingPackets.clear();
+
+ m_pingTime.reset();
+
+ m_entityMap.reset();
+ m_worldTemplate.reset();
+ m_worldProperties.clear();
+
+ m_tileArray.reset();
+
+ m_damageManager.reset();
+ m_luaRoot.reset();
+
+ m_particles.reset();
+
+ m_sky.reset();
+
+ m_currentParallax.reset();
+ m_nextParallax.reset();
+ m_parallaxFadeTimer.setDone();
+
+ m_clientState.reset();
+ m_ambientSounds.cancelAll();
+ m_musicTrack.cancelAll();
+ m_musicTrack.setVolume(1, 0, 0);
+ m_altMusicTrack.cancelAll();
+ m_altMusicTrack.setVolume(0, 0, 0);
+ m_altMusicActive = false;
+
+ if (m_spaceSound) {
+ m_spaceSound->stop();
+ m_spaceSound = {};
+ }
+
+ m_entityMessageResponses = {};
+
+ m_forceRegions.clear();
+}
+
+void WorldClient::tryGiveMainPlayerItem(ItemPtr item) {
+ if (auto spill = m_mainPlayer->pickupItems(item))
+ addEntity(ItemDrop::createRandomizedDrop(spill->descriptor(), m_mainPlayer->position()));
+}
+
+Vec2I WorldClient::environmentBiomeTrackPosition() const {
+ if (!inWorld())
+ return {};
+
+ auto pos = Vec2I::floor(m_clientState.windowCenter());
+ return {m_geometry.xwrap(pos[0]), pos[1]};
+}
+
+AmbientNoisesDescriptionPtr WorldClient::currentAmbientNoises() const {
+ if (!inWorld())
+ return {};
+
+ Vec2I pos = environmentBiomeTrackPosition();
+ return m_worldTemplate->ambientNoises(pos[0], pos[1]);
+}
+
+WeatherNoisesDescriptionPtr WorldClient::currentWeatherNoises() const {
+ if (!inWorld())
+ return {};
+
+ auto trackOptions = m_weather.weatherTrackOptions();
+ if (trackOptions.empty())
+ return {};
+ else
+ return make_shared<WeatherNoisesDescription>(move(trackOptions));
+}
+
+AmbientNoisesDescriptionPtr WorldClient::currentMusicTrack() const {
+ if (!inWorld())
+ return {};
+
+ Vec2I pos = environmentBiomeTrackPosition();
+ return m_worldTemplate->musicTrack(pos[0], pos[1]);
+}
+
+AmbientNoisesDescriptionPtr WorldClient::currentAltMusicTrack() const {
+ if (!inWorld())
+ return {};
+
+ return m_altMusicTrackDescription;
+}
+
+void WorldClient::playAltMusic(StringList const& newTracks, float fadeTime) {
+ auto newTrackGroup = AmbientTrackGroup(newTracks);
+ m_altMusicTrackDescription = make_shared<AmbientNoisesDescription>(AmbientTrackGroup(newTracks), AmbientTrackGroup());
+ if (!m_altMusicActive) {
+ m_musicTrack.setVolume(0.0, 0.0, fadeTime);
+ m_altMusicTrack.setVolume(1.0, 0.0, fadeTime);
+ m_altMusicActive = true;
+ }
+}
+
+void WorldClient::stopAltMusic(float fadeTime) {
+ if (m_altMusicActive) {
+ m_musicTrack.setVolume(1.0, 0.0, fadeTime);
+ m_altMusicTrack.setVolume(0.0, 0.0, fadeTime);
+ m_altMusicActive = false;
+ }
+}
+
+BiomeConstPtr WorldClient::mainEnvironmentBiome() const {
+ if (!inWorld())
+ return {};
+
+ Vec2I pos = environmentBiomeTrackPosition();
+ return m_worldTemplate->environmentBiome(pos[0], pos[1]);
+}
+
+bool WorldClient::readNetTile(Vec2I const& pos, NetTile const& netTile) {
+ ClientTile* tile = m_tileArray->modifyTile(pos);
+ if (!tile)
+ return false;
+
+ m_predictedTiles.remove(pos);
+
+ tile->background = netTile.background;
+ tile->backgroundHueShift = netTile.backgroundHueShift;
+ tile->backgroundColorVariant = netTile.backgroundColorVariant;
+ tile->backgroundMod = netTile.backgroundMod;
+ tile->backgroundModHueShift = netTile.backgroundModHueShift;
+ tile->foreground = netTile.foreground;
+ tile->foregroundHueShift = netTile.foregroundHueShift;
+ tile->foregroundColorVariant = netTile.foregroundColorVariant;
+ tile->foregroundMod = netTile.foregroundMod;
+ tile->foregroundModHueShift = netTile.foregroundModHueShift;
+ tile->collision = netTile.collision;
+ tile->blockBiomeIndex = netTile.blockBiomeIndex;
+ tile->environmentBiomeIndex = netTile.environmentBiomeIndex;
+ tile->liquid = netTile.liquid.liquidLevel();
+ tile->dungeonId = netTile.dungeonId;
+
+ auto materialDatabase = Root::singleton().materialDatabase();
+ tile->backgroundLightTransparent = materialDatabase->backgroundLightTransparent(tile->background);
+ tile->foregroundLightTransparent =
+ materialDatabase->foregroundLightTransparent(tile->foreground) && tile->collision != CollisionKind::Dynamic;
+
+ dirtyCollision(RectI::withSize(pos, {1, 1}));
+
+ return true;
+}
+
+void WorldClient::dirtyCollision(RectI const& region) {
+ if (!inWorld())
+ return;
+
+ auto dirtyRegion = region.padded(CollisionGenerator::BlockInfluenceRadius);
+ for (int x = dirtyRegion.xMin(); x < dirtyRegion.xMax(); ++x) {
+ for (int y = dirtyRegion.yMin(); y < dirtyRegion.yMax(); ++y) {
+ if (auto tile = m_tileArray->modifyTile({x, y}))
+ tile->collisionCacheDirty = true;
+ }
+ }
+}
+
+void WorldClient::freshenCollision(RectI const& region) {
+ if (!inWorld())
+ return;
+
+ RectI freshenRegion = RectI::null();
+ for (int x = region.xMin(); x < region.xMax(); ++x) {
+ for (int y = region.yMin(); y < region.yMax(); ++y) {
+ if (auto tile = m_tileArray->modifyTile({x, y})) {
+ if (tile->collisionCacheDirty)
+ freshenRegion.combine(RectI(x, y, x + 1, y + 1));
+ }
+ }
+ }
+
+ if (!freshenRegion.isNull()) {
+ for (int x = freshenRegion.xMin(); x < freshenRegion.xMax(); ++x) {
+ for (int y = freshenRegion.yMin(); y < freshenRegion.yMax(); ++y) {
+ if (auto tile = m_tileArray->modifyTile({x, y})) {
+ tile->collisionCacheDirty = false;
+ tile->collisionCache.clear();
+ }
+ }
+ }
+
+ for (auto collisionBlock : m_collisionGenerator.getBlocks(freshenRegion)) {
+ if (auto tile = m_tileArray->modifyTile(collisionBlock.space))
+ tile->collisionCache.append(move(collisionBlock));
+ }
+ }
+}
+
+float WorldClient::lightLevel(Vec2F const& pos) const {
+ if (!inWorld())
+ return 0.0f;
+ return WorldImpl::lightLevel(m_tileArray, m_entityMap, m_geometry, m_worldTemplate, m_sky, m_lightIntensityCalculator, pos);
+}
+
+bool WorldClient::breathable(Vec2F const& pos) const {
+ if (!inWorld())
+ return true;
+
+ return WorldImpl::breathable(this, m_tileArray, m_dungeonIdBreathable, m_worldTemplate, pos);
+}
+
+float WorldClient::threatLevel() const {
+ if (!inWorld())
+ return 0.0f;
+ return m_worldTemplate->threatLevel();
+}
+
+StringList WorldClient::environmentStatusEffects(Vec2F const& pos) const {
+ if (!inWorld())
+ return {};
+
+ return m_worldTemplate->environmentStatusEffects(floor(pos[0]), floor(pos[1]));
+}
+
+StringList WorldClient::weatherStatusEffects(Vec2F const& pos) const {
+ if (!inWorld())
+ return {};
+
+ if (!m_weather.statusEffects().empty()) {
+ if (exposedToWeather(pos))
+ return m_weather.statusEffects();
+ }
+
+ return {};
+}
+
+bool WorldClient::exposedToWeather(Vec2F const& pos) const {
+ if (!inWorld())
+ return false;
+
+ if (!isUnderground(pos) && liquidLevel(Vec2I::floor(pos)).liquid == EmptyLiquidId) {
+ auto assets = Root::singleton().assets();
+ float weatherRayCheckDistance = assets->json("/weather.config:weatherRayCheckDistance").toFloat();
+ float weatherRayCheckWindInfluence = assets->json("/weather.config:weatherRayCheckWindInfluence").toFloat();
+
+ auto offset = Vec2F(-m_weather.wind() * weatherRayCheckWindInfluence, weatherRayCheckDistance).normalized() * weatherRayCheckDistance;
+
+ return !lineCollision({pos, pos + offset});
+ }
+
+ return false;
+}
+
+bool WorldClient::isUnderground(Vec2F const& pos) const {
+ if (!inWorld())
+ return true;
+ return m_worldTemplate->undergroundLevel() >= pos[1];
+}
+
+bool WorldClient::disableDeathDrops() const {
+ if (m_worldTemplate->worldParameters())
+ return m_worldTemplate->worldParameters()->disableDeathDrops;
+ return false;
+}
+
+List<PhysicsForceRegion> WorldClient::forceRegions() const {
+ return m_forceRegions;
+}
+
+Json WorldClient::getProperty(String const& propertyName, Json const& def) const {
+ if (!inWorld())
+ return {};
+
+ return m_worldProperties.value(propertyName, def);
+}
+
+void WorldClient::setProperty(String const& propertyName, Json const& property) {
+ if (!inWorld())
+ return;
+
+ if (m_worldProperties[propertyName] == property)
+ return;
+
+ m_outgoingPackets.append(make_shared<UpdateWorldPropertiesPacket>(JsonObject{{propertyName, property}}));
+}
+
+bool WorldClient::playerCanReachEntity(EntityId entityId, bool preferInteractive) const {
+ return canReachEntity(m_mainPlayer->position(), m_mainPlayer->interactRadius(), entityId, preferInteractive);
+}
+
+void WorldClient::disconnectAllWires(Vec2I wireEntityPosition, WireNode const& node) {
+ m_outgoingPackets.append(make_shared<DisconnectAllWiresPacket>(wireEntityPosition, node));
+}
+
+void WorldClient::connectWire(WireConnection const& output, WireConnection const& input) {
+ m_outgoingPackets.append(make_shared<ConnectWirePacket>(output, input));
+}
+
+void WorldClient::ClientRenderCallback::addDrawable(Drawable drawable, EntityRenderLayer renderLayer) {
+ drawables[renderLayer].append(move(drawable));
+}
+
+void WorldClient::ClientRenderCallback::addLightSource(LightSource lightSource) {
+ lightSources.append(move(lightSource));
+}
+
+void WorldClient::ClientRenderCallback::addParticle(Particle particle) {
+ particles.append(move(particle));
+}
+
+void WorldClient::ClientRenderCallback::addAudio(AudioInstancePtr audio) {
+ audios.append(move(audio));
+}
+
+void WorldClient::ClientRenderCallback::addTilePreview(PreviewTile preview) {
+ previewTiles.append(move(preview));
+}
+
+void WorldClient::ClientRenderCallback::addOverheadBar(OverheadBar bar) {
+ overheadBars.append(move(bar));
+}
+
+double WorldClient::epochTime() const {
+ if (!inWorld())
+ return 0;
+ return m_sky->epochTime();
+}
+
+uint32_t WorldClient::day() const {
+ if (!inWorld())
+ return 0;
+ return m_sky->day();
+}
+
+float WorldClient::dayLength() const {
+ if (!inWorld())
+ return 0;
+ return m_sky->dayLength();
+}
+
+float WorldClient::timeOfDay() const {
+ if (!inWorld())
+ return 0;
+ return m_sky->timeOfDay();
+}
+
+LuaRootPtr WorldClient::luaRoot() {
+ return m_luaRoot;
+}
+
+RpcPromise<Vec2F> WorldClient::findUniqueEntity(String const& uniqueId) {
+ if (auto entity = m_entityMap->uniqueEntity(uniqueId))
+ return RpcPromise<Vec2F>::createFulfilled(entity->position());
+
+ auto pair = RpcPromise<Vec2F>::createPair();
+ auto& rpcPromises = m_findUniqueEntityResponses[uniqueId];
+ if (rpcPromises.empty())
+ m_outgoingPackets.append(make_shared<FindUniqueEntityPacket>(uniqueId));
+ rpcPromises.append(pair.second);
+
+ return pair.first;
+}
+
+RpcPromise<Json> WorldClient::sendEntityMessage(Variant<EntityId, String> const& entityId, String const& message, JsonArray const& args) {
+ EntityPtr entity;
+ if (entityId.is<EntityId>())
+ entity = m_entityMap->entity(entityId.get<EntityId>());
+ else
+ entity = m_entityMap->uniqueEntity(entityId.get<String>());
+
+ // Only fail with "unknown entity" if we know this entity should exist on the
+ // client, because it's entity id indicates it is master here.
+ if (entityId.is<EntityId>() && !entity && m_clientId == connectionForEntity(entityId.get<EntityId>())) {
+ return RpcPromise<Json>::createFailed("Unknown entity");
+ } else if (entity && entity->isMaster()) {
+ if (auto resp = entity->receiveMessage(*m_clientId, message, args))
+ return RpcPromise<Json>::createFulfilled(resp.take());
+ else
+ return RpcPromise<Json>::createFailed("Message not handled by entity");
+ } else {
+ auto pair = RpcPromise<Json>::createPair();
+ Uuid uuid;
+ m_entityMessageResponses[uuid] = pair.second;
+ m_outgoingPackets.append(make_shared<EntityMessagePacket>(entityId, message, args, uuid));
+ return pair.first;
+ }
+}
+
+List<ChatAction> WorldClient::pullPendingChatActions() {
+ List<ChatAction> result;
+ if (m_entityMap) {
+ for (auto const& entity : m_entityMap->all<ChattyEntity>())
+ result.appendAll(entity->pullPendingChatActions());
+ }
+ return result;
+}
+
+WorldStructure const& WorldClient::centralStructure() const {
+ return m_centralStructure;
+}
+
+bool WorldClient::DamageNumberKey::operator<(DamageNumberKey const& other) const {
+ return tie(sourceEntityId, targetEntityId, damageNumberParticleKind)
+ < tie(other.sourceEntityId, other.targetEntityId, other.damageNumberParticleKind);
+}
+
+void WorldClient::renderCollisionDebug() {
+ RectI clientWindow = m_clientState.window();
+ if (clientWindow.isEmpty())
+ return;
+
+ auto logPoly = [](PolyF poly, Vec2F position, float r, float g, float b) {
+ poly.translate(position);
+ SpatialLogger::logPoly("world", poly, {floatToByte(r, true), floatToByte(g, true), floatToByte(b, true), 255});
+ };
+
+ forEachCollisionBlock(clientWindow, [&](auto const& block) {
+ logPoly(block.poly, Vec2F{}, 1.0f, 0.0f, 0.0f);
+ });
+
+ for (auto const& object : query<TileEntity>(RectF(clientWindow))) {
+ for (auto const& space : object->spaces())
+ logPoly(PolyF(RectF(Vec2F(space), Vec2F(space) + Vec2F(1, 1))), Vec2F(object->tilePosition()), 0., 1., 0.);
+ }
+
+ for (auto const& physics : query<PhysicsEntity>(RectF(clientWindow))) {
+ for (auto const& forceRegion : physics->forceRegions()) {
+ if (auto dfr = forceRegion.ptr<DirectionalForceRegion>())
+ logPoly(dfr->region, {}, 1.0f, 1.0f, 0.0f);
+ else if (auto rfr = forceRegion.ptr<RadialForceRegion>())
+ logPoly(PolyF(rfr->boundBox()), {}, 0.0f, 1.0f, 1.0f);
+ }
+
+ for (size_t i = 0; i < physics->movingCollisionCount(); ++i) {
+ if (auto pmc = physics->movingCollision(i)) {
+ logPoly(pmc->collision, pmc->position, 1.0f, 1.0f, 1.0f);
+ }
+ }
+ }
+}
+
+void WorldClient::setupForceRegions() {
+ m_forceRegions.clear();
+
+ if (!currentTemplate() || !currentTemplate()->worldParameters())
+ return;
+
+ auto forceRegionType = currentTemplate()->worldParameters()->worldEdgeForceRegions;
+
+ if (forceRegionType == WorldEdgeForceRegionType::None)
+ return;
+
+ bool addTopRegion = forceRegionType == WorldEdgeForceRegionType::Top || forceRegionType == WorldEdgeForceRegionType::TopAndBottom;
+ bool addBottomRegion = forceRegionType == WorldEdgeForceRegionType::Bottom || forceRegionType == WorldEdgeForceRegionType::TopAndBottom;
+
+ auto worldServerConfig = Root::singleton().assets()->json("/worldserver.config");
+
+ auto regionHeight = worldServerConfig.getFloat("worldEdgeForceRegionHeight");
+ auto regionForce = worldServerConfig.getFloat("worldEdgeForceRegionForce");
+ auto regionVelocity = worldServerConfig.getFloat("worldEdgeForceRegionVelocity");
+ auto regionCategoryFilter = PhysicsCategoryFilter::whitelist({"player", "monster", "npc", "vehicle"});
+ auto worldSize = Vec2F(currentTemplate()->size());
+
+ if (addTopRegion) {
+ auto topForceRegion = GradientForceRegion();
+ topForceRegion.region = PolyF({
+ {0, worldSize[1] - regionHeight},
+ {worldSize[0], worldSize[1] - regionHeight},
+ (worldSize),
+ {0, worldSize[1]}});
+ topForceRegion.gradient = Line2F({0, worldSize[1]}, {0, worldSize[1] - regionHeight});
+ topForceRegion.baseTargetVelocity = regionVelocity;
+ topForceRegion.baseControlForce = regionForce;
+ topForceRegion.categoryFilter = regionCategoryFilter;
+ m_forceRegions.append(topForceRegion);
+ }
+
+ if (addBottomRegion) {
+ auto bottomForceRegion = GradientForceRegion();
+ bottomForceRegion.region = PolyF({
+ {0, 0},
+ {worldSize[0], 0},
+ {worldSize[0], regionHeight},
+ {0, regionHeight}});
+ bottomForceRegion.gradient = Line2F({0, 0}, {0, regionHeight});
+ bottomForceRegion.baseTargetVelocity = regionVelocity;
+ bottomForceRegion.baseControlForce = regionForce;
+ bottomForceRegion.categoryFilter = regionCategoryFilter;
+ m_forceRegions.append(bottomForceRegion);
+ }
+}
+
+}