#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" #include "StarCurve25519.hpp" namespace Star { const std::string SECRET_BROADCAST_PUBLIC_KEY = "SecretBroadcastPublicKey"; const std::string SECRET_BROADCAST_PREFIX = "\0Broadcast\0"s; 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_currentTime = 0; m_fullBright = false; m_asyncLighting = 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_luaRoot = make_shared(); m_mainPlayer = mainPlayer; centerClientWindowOnPlayer(Vec2U(100, 100)); m_collisionGenerator.init([this](int x, int y) { if (!m_predictedTiles.empty()) { if (auto p = m_predictedTiles.ptr({x, y})) { if (p->collision) return *p->collision; } } return m_tileArray->tile({x, y}).collision; }); m_modifiedTilePredictionTimeout = (int)round(m_clientConfig.getFloat("modifiedTilePredictionTimeout") / GlobalTimestep); 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; m_stopLightingThread = false; clearWorld(); } WorldClient::~WorldClient() { if (m_lightingThread) { m_stopLightingThread = true; { MutexLocker locker(m_lightingMutex); m_lightingCond.broadcast(); } m_lightingThread.finish(); } 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(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; } void WorldClient::removeEntity(EntityId entityId, bool andDie) { auto entity = m_entityMap->entity(entityId); if (!entity) return; if (andDie) { ClientRenderCallback renderCallback; entity->destroy(&renderCallback); const List* directives = nullptr; if (auto& worldTemplate = m_worldTemplate) { if (const auto& parameters = worldTemplate->worldParameters()) if (auto& globalDirectives = m_worldTemplate->worldParameters()->globalDirectives) directives = &globalDirectives.get(); } if (directives) { int directiveIndex = unsigned(entity->entityId()) % directives->size(); for (auto& p : renderCallback.particles) p.directives.append(directives->get(directiveIndex)); } m_particles->addParticles(std::move(renderCallback.particles)); m_samples.appendAll(std::move(renderCallback.audios)); } if (auto version = m_masterEntitiesNetVersion.maybeTake(entity->entityId())) { ByteArray finalNetState = entity->writeNetState(*version).first; m_outgoingPackets.append(make_shared(entity->entityId(), std::move(finalNetState), andDie)); } m_entityMap->removeEntity(entityId); entity->uninit(); } WorldTemplateConstPtr WorldClient::currentTemplate() const { return m_worldTemplate; } SkyConstPtr WorldClient::currentSky() const { return m_sky; } void WorldClient::timer(float delay, WorldAction worldAction) { if (!inWorld()) return; m_timers.append({delay, 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 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 entityFilter) const { if (!inWorld()) return {}; return m_entityMap->findEntityAtTile(pos, entityFilter); } bool WorldClient::tileIsOccupied(Vec2I const& pos, TileLayer layer, bool includeEphemeral, bool checkCollision) const { if (!inWorld()) return false; return WorldImpl::tileIsOccupied(m_tileArray, m_entityMap, pos, layer, includeEphemeral, checkCollision); } CollisionKind WorldClient::tileCollisionKind(Vec2I const& pos) const { if (!inWorld()) return CollisionKind::Null; return WorldImpl::tileCollisionKind(m_tileArray, m_entityMap, pos); } void WorldClient::forEachCollisionBlock(RectI const& region, function const& iterator) const { if (!inWorld()) return; const_cast(this)->freshenCollision(region); m_tileArray->tileEach(region, [iterator](Vec2I const& pos, ClientTile const& tile) { if (tile.getCollision() == 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> 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 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_entityMap, modificationList, allowEntityOverlap, m_tileGetterFunction, [this](Vec2I pos, TileModification) { return !isTileProtected(pos); }).first; } TileModificationList WorldClient::applyTileModifications(TileModificationList const& modificationList, bool allowEntityOverlap) { if (!inWorld()) return {}; // thanks to new prediction: do each one by one so that previous modifications affect placeability TileModificationList success, failures, temp; TileModificationList const* list = &modificationList; while (true) { bool yay = false; for (size_t i = 0; i != list->size(); ++i) { auto& pair = list->at(i); if (!isTileProtected(pair.first)) { auto result = WorldImpl::validateTileModification(m_entityMap, pair.first, pair.second, allowEntityOverlap, m_tileGetterFunction); if (result.first) { informTilePrediction(pair.first, pair.second); success.append(pair); yay = true; continue; } } failures.append(pair); } if (yay) { list = &(temp = std::move(failures)); failures = {}; continue; } else break; } if (!success.empty()) m_outgoingPackets.append(make_shared(std::move(success), true)); return failures; } 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(); } WorldClientState& WorldClient::clientState() { return m_clientState; } void WorldClient::render(WorldRenderData& renderData, unsigned bufferTiles) { if (!m_lightingThread && m_asyncLighting) m_lightingThread = Thread::invoke("WorldClient::lightingMain", mem_fn(&WorldClient::lightingMain), this); 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 renderLightSources; m_previewTiles.clear(); renderData.geometry = m_geometry; ClientRenderCallback lightingRenderCallback; m_entityMap->forAllEntities([&](EntityPtr const& entity) { if (m_startupHiddenEntities.contains(entity->entityId())) return; entity->renderLightSources(&lightingRenderCallback); }); renderLightSources = std::move(lightingRenderCallback.lightSources); RectI window = m_clientState.window(); RectI tileRange = window.padded(bufferTiles); renderData.tileMinPosition = tileRange.min(); if (!m_fullBright) { { MutexLocker m_prepLocker(m_lightMapPrepMutex); m_pendingLights = std::move(renderLightSources); m_pendingParticleLights = std::move(m_particles->lightSources()); m_pendingLightRange = window.padded(1); } //Kae: Padded by one to fix light spread issues at the edges of the frame. if (m_asyncLighting) m_lightingCond.signal(); else lightingCalc(); } 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(); } const List* directives = nullptr; if (auto& worldTemplate = m_worldTemplate) { if (const auto& parameters = worldTemplate->worldParameters()) if (auto& globalDirectives = m_worldTemplate->worldParameters()->globalDirectives) directives = &globalDirectives.get(); } m_entityMap->forAllEntities([&](EntityPtr const& entity) { if (m_startupHiddenEntities.contains(entity->entityId())) return; ClientRenderCallback renderCallback; try { entity->render(&renderCallback); } catch (StarException const& e) { if (entity->isMaster()) // this is YOUR problem!! throw e; else { // this is THEIR problem!! Logger::error("WorldClient: Exception caught in {}::render ({}): {}", EntityTypeNames.getRight(entity->entityType()), entity->entityId(), e.what()); auto toolUser = as(entity); String image = toolUser ? strf("/rendering/error_{}.png", DirectionNames.getRight(toolUser->facingDirection())) : "/rendering/error.png"; Color color = Color::rgbf(0.8f + (float)sin(m_currentTime * Constants::pi * 0.5) * 0.2f, 0.0f, 0.0f); auto drawable = Drawable::makeImage(image, 1.0f / TilePixels, true, entity->position(), color); drawable.fullbright = true; renderCallback.addDrawable(std::move(drawable), RenderLayerMiddleParticle); } } 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] = std::move(p.second); } if (m_interactiveHighlightMode || (!inspecting && entity->entityId() == playerAimInteractive)) { if (auto interactive = as(entity)) { if (interactive->isInteractive()) { ed.highlightEffect.type = EntityHighlightEffectType::Interactive; ed.highlightEffect.level = pulseLevel; } } } else if (inspecting) { if (auto inspectable = as(entity)) { ed.highlightEffect = m_mainPlayer->inspectionHighlight(inspectable); ed.highlightEffect.level *= inspectionFlickerMultiplier; } } renderData.entityDrawables.append(std::move(ed)); if (directives) { int directiveIndex = unsigned(entity->entityId()) % directives->size(); for (auto& p : renderCallback.particles) p.directives.append(directives->get(directiveIndex)); } m_particles->addParticles(std::move(renderCallback.particles)); m_samples.appendAll(std::move(renderCallback.audios)); m_previewTiles.appendAll(std::move(renderCallback.previewTiles)); renderData.overheadBars.appendAll(std::move(renderCallback.overheadBars)); }, [](EntityPtr const& a, EntityPtr const& b) { return a->entityId() < b->entityId(); }); m_tileArray->tileEachTo(renderData.tiles, tileRange, [&](RenderTile& renderTile, Vec2I const& position, 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); }); for (auto& pair : m_predictedTiles) { Vec2I tileArrayPos = m_geometry.diff(pair.first, 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]); PredictedTile& p = pair.second; if (p.liquid) { auto& liquid = *p.liquid; if (liquid.liquid == renderTile.liquidId) { uint8_t added = floatToByte(liquid.level, true); renderTile.liquidLevel = (renderTile.liquidLevel > 255 - added) ? 255 : renderTile.liquidLevel + added; } else { renderTile.liquidId = liquid.liquid; renderTile.liquidLevel = floatToByte(liquid.level, true); } } pair.second.apply(renderTile); } } for (auto const& previewTile : m_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 = 255; } } } renderData.particles = &m_particles->particles(); LogMap::set("client_render_particle_count", 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); } } auto functionDatabase = Root::singleton().functionDatabase(); for (auto& layer : renderData.parallaxLayers) { if (!layer.timeOfDayCorrelation.empty()) layer.alpha *= clamp((float)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 WorldClient::pullPendingAudio() { return take(m_samples); } List 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::fullBright() const { return m_fullBright; } void WorldClient::setFullBright(bool fullBright) { m_fullBright = fullBright; } bool WorldClient::asyncLighting() const { return m_asyncLighting; } void WorldClient::setAsyncLighting(bool asyncLighting) { m_asyncLighting = asyncLighting; } bool WorldClient::collisionDebug() const { return m_collisionDebug; } void WorldClient::setCollisionDebug(bool collisionDebug) { m_collisionDebug = collisionDebug; } void WorldClient::handleIncomingPackets(List 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(packet)) Logger::error("WorldClient received packet type {} while not in world", PacketTypeNames.getRight(packet->type())); if (auto worldStartPacket = as(packet)) { initWorld(*worldStartPacket); } else if (auto worldStopPacket = as(packet)) { Logger::info("Client received world stop packet, leaving: {}", worldStopPacket->reason); clearWorld(); } else if (auto entityCreate = as(packet)) { if (m_entityMap->entity(entityCreate->entityId)) { Logger::error("WorldClient received entity create packet with duplicate entity id {}, 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(m_interpolationTracker.interpolationLeadTime(), [this, entityId = entityCreate->entityId](World*) { m_startupHiddenEntities.remove(entityId); }); } } else if (auto entityUpdateSet = as(packet)) { float interpolationLeadTime = m_interpolationTracker.interpolationLeadTime(); 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(packet)) { if (auto entity = m_entityMap->entity(entityDestroy->entityId)) { entity->readNetState(entityDestroy->finalNetState, m_interpolationTracker.interpolationLeadTime()); // 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(m_interpolationTracker.interpolationLeadTime(), [this, entity, entityDestroy](World*) { entity->disableInterpolation(); removeEntity(entityDestroy->entityId, entityDestroy->death); }); } else { entity->disableInterpolation(); removeEntity(entityDestroy->entityId, entityDestroy->death); } } } else if (auto structurePacket = as(packet)) { m_centralStructure = WorldStructure(structurePacket->structureData); } else if (auto tileArrayUpdate = as(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()), false); } dirtyCollision(tileRegion); } else if (auto tileUpdate = as(packet)) { readNetTile(tileUpdate->position, tileUpdate->tile); } else if (auto tileDamageUpdate = as(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(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) { auto findPrediction = m_predictedTiles.find(modification.first); if (findPrediction != m_predictedTiles.end()) { auto& p = findPrediction->second; if (auto placeMaterial = modification.second.ptr()) { if (placeMaterial->layer == TileLayer::Foreground) { p.foreground.reset(); p.foregroundHueShift.reset(); if (p.collision) { p.collision.reset(); dirtyCollision(RectI::withSize(modification.first, { 1, 1 })); } } else { p.background.reset(); p.backgroundHueShift.reset(); } } else if (auto placeMod = modification.second.ptr()) { if (placeMod->layer == TileLayer::Foreground) { p.foregroundMod.reset(); p.foregroundModHueShift.reset(); } else { p.backgroundMod.reset(); p.backgroundModHueShift.reset(); } } else if (auto placeColor = modification.second.ptr()) { if (placeColor->layer == TileLayer::Foreground) p.foregroundColorVariant.reset(); else p.backgroundColorVariant.reset(); } else if (auto placeLiquid = modification.second.ptr()) { p.liquid.reset(); } if (!p) m_predictedTiles.erase(findPrediction); } if (auto placeMaterial = modification.second.ptr()) { auto stack = materialDatabase->materialItemDrop(placeMaterial->material); tryGiveMainPlayerItem(itemDatabase->item(stack), true); } else if (auto placeMod = modification.second.ptr()) { auto stack = materialDatabase->modItemDrop(placeMod->mod); tryGiveMainPlayerItem(itemDatabase->item(stack), true); } } } else if (auto liquidUpdate = as(packet)) { m_predictedTiles.remove(liquidUpdate->position); if (ClientTile* tile = m_tileArray->modifyTile(liquidUpdate->position)) tile->liquid = liquidUpdate->liquidUpdate.liquidLevel(); } else if (auto giveItem = as(packet)) { tryGiveMainPlayerItem(itemDatabase->item(giveItem->item)); } else if (auto stepUpdate = as(packet)) { m_interpolationTracker.receiveTimeUpdate(stepUpdate->remoteTime); } else if (auto environmentUpdatePacket = as(packet)) { m_sky->readUpdate(environmentUpdatePacket->skyDelta); m_weather.readUpdate(environmentUpdatePacket->weatherDelta); } else if (auto hit = as(packet)) { m_damageManager->pushRemoteHitRequest(hit->remoteHitRequest); } else if (auto damage = as(packet)) { m_damageManager->pushRemoteDamageRequest(damage->remoteDamageRequest); } else if (auto damage = as(packet)) { std::string_view view = damage->remoteDamageNotification.damageNotification.targetMaterialKind.utf8(); static const size_t FULL_SIZE = SECRET_BROADCAST_PREFIX.size() + Curve25519::SignatureSize; static const std::string LEGACY_VOICE_PREFIX = "data\0voice\0"s; if (view.size() >= FULL_SIZE && view.rfind(SECRET_BROADCAST_PREFIX, 0) != NPos) { // this is actually a secret broadcast!! if (auto player = m_entityMap->get(damage->remoteDamageNotification.sourceEntityId)) { if (auto publicKey = player->getSecretPropertyView(SECRET_BROADCAST_PUBLIC_KEY)) { if (publicKey->utf8Size() == Curve25519::PublicKeySize) { auto signature = view.substr(SECRET_BROADCAST_PREFIX.size(), Curve25519::SignatureSize); auto rawBroadcast = view.substr(FULL_SIZE); if (Curve25519::verify( (uint8_t const*)signature.data(), (uint8_t const*)publicKey->utf8Ptr(), (void*)rawBroadcast.data(), rawBroadcast.size() )) { handleSecretBroadcast(player, rawBroadcast); } } } } } else if (view.size() > 75 && view.rfind(LEGACY_VOICE_PREFIX, 0) != NPos) { // this is a StarExtensions voice packet // (remove this and stop transmitting like this once most SE features are ported over) if (auto player = m_entityMap->get(damage->remoteDamageNotification.sourceEntityId)) { if (auto publicKey = player->effectsAnimator()->globalTagPtr("\0SE_VOICE_SIGNING_KEY"s)) { auto raw = view.substr(75); if (m_broadcastCallback && Curve25519::verify( (uint8_t const*)view.data() + LEGACY_VOICE_PREFIX.size(), (uint8_t const*)publicKey->utf8Ptr(), (void*)raw.data(), raw.size() )) { auto broadcastData = "Voice\0"s; broadcastData.append(raw.data(), raw.size()); m_broadcastCallback(player, broadcastData); } } } } else { m_damageManager->pushRemoteDamageNotification(damage->remoteDamageNotification); } } else if (auto entityMessagePacket = as(packet)) { EntityPtr entity; if (entityMessagePacket->entityId.is()) entity = m_entityMap->entity(entityMessagePacket->entityId.get()); else entity = m_entityMap->uniqueEntity(entityMessagePacket->entityId.get()); if (!entity) { m_outgoingPackets.append(make_shared(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(makeLeft("Entity delivery error"), entityMessagePacket->uuid)); } else { ConnectionId fromConnection = entityMessagePacket->fromConnection; if (fromConnection == *m_clientId) // Kae: The server should not be able to forge entity messages that appear as if they're from us fromConnection = ServerConnectionId; auto response = entity->receiveMessage(entityMessagePacket->fromConnection, entityMessagePacket->message, entityMessagePacket->args); if (response) m_outgoingPackets.append(make_shared(makeRight(response.take()), entityMessagePacket->uuid)); else m_outgoingPackets.append(make_shared(makeLeft("Message not handled by entity"), entityMessagePacket->uuid)); } } else if (auto entityMessageResponsePacket = as(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(packet)) { // Kae: Properties set to null (nil from Lua) should be erased instead of lingering around for (auto& pair : updateWorldProperties->updatedProperties) { if (pair.second.isNull()) m_worldProperties.erase(pair.first); else m_worldProperties[pair.first] = pair.second; } } else if (auto updateTileProtection = as(packet)) { setTileProtection(updateTileProtection->dungeonId, updateTileProtection->isProtected); } else if (auto setDungeonGravity = as(packet)) { if (setDungeonGravity->gravity) m_dungeonIdGravity[setDungeonGravity->dungeonId] = *setDungeonGravity->gravity; else m_dungeonIdGravity.remove(setDungeonGravity->dungeonId); } else if (auto setDungeonBreathable = as(packet)) { if (setDungeonBreathable->breathable.isValid()) m_dungeonIdBreathable[setDungeonBreathable->dungeonId] = *setDungeonBreathable->breathable; else m_dungeonIdBreathable.remove(setDungeonBreathable->dungeonId); } else if (auto entityInteract = as(packet)) { auto interactResult = interact(entityInteract->interactRequest).result(); m_outgoingPackets.append(make_shared(interactResult.take(), entityInteract->requestId, entityInteract->interactRequest.sourceId)); } else if (auto interactResult = as(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(packet)) { m_playerStart = setPlayerStart->playerStart; m_respawnInWorld = setPlayerStart->respawnInWorld; } else if (auto findUniqueEntityResponse = as(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(packet)) { m_worldTemplate->setWorldLayout(make_shared(worldLayoutUpdate->layoutData)); } else if (auto worldParametersUpdate = as(packet)) { m_worldTemplate->setWorldParameters(netLoadVisitableWorldParameters(worldParametersUpdate->parametersData)); } else if (auto pongPacket = as(packet)) { if (pongPacket->time) m_latency = Time::monotonicMilliseconds() - pongPacket->time; else if (m_pingTime) m_latency = Time::monotonicMilliseconds() - m_pingTime.take(); } else { Logger::error("Improper packet type {} received by client", (int)packet->type()); } } } List WorldClient::getOutgoingPackets() { return std::move(m_outgoingPackets); } void WorldClient::setLuaCallbacks(String const& groupName, LuaCallbacks const& callbacks) { m_luaRoot->addCallbacks(groupName, callbacks); } void WorldClient::update(float dt) { if (!inWorld()) return; auto assets = Root::singleton().assets(); float expireTime = min(float(m_latency + 800), 2000.f); auto now = Time::monotonicMilliseconds(); eraseWhere(m_predictedTiles, [&](auto& pair) { float expiry = (float)(now - pair.second.time) / expireTime; auto center = Vec2F(pair.first) + Vec2F::filled(0.5f); auto size = Vec2F::filled(0.875f - expiry * 0.875f); auto poly = PolyF(RectF::withCenter(center, size)); SpatialLogger::logPoly("world", poly, Color::Cyan.mix(Color::Red, expiry).toRgba()); if (expiry >= 1.0f) { dirtyCollision(RectI::withSize(pair.first, { 1, 1 })); return true; } else { return false; } }); // Secret broadcasts are transmitted through DamageNotifications for vanilla server compatibility. // Because DamageNotification packets are spoofable, we have to sign the data so other clients can validate that it is legitimate. auto& publicKey = Curve25519::publicKey(); String publicKeyString((const char*)publicKey.data(), publicKey.size()); m_mainPlayer->setSecretProperty(SECRET_BROADCAST_PUBLIC_KEY, publicKeyString); // Temporary: Backwards compatibility with StarExtensions m_mainPlayer->effectsAnimator()->setGlobalTag("\0SE_VOICE_SIGNING_KEY"s, publicKeyString); ++m_currentStep; m_currentTime += dt; m_interpolationTracker.update(m_currentTime); List triggeredActions; eraseWhere(m_timers, [&triggeredActions, dt](pair& timer) { if ((timer.first -= dt) <= 0) { triggeredActions.append(timer.second); return true; } return false; }); for (auto const& action : triggeredActions) action(this); List toRemove; List clientPresenceEntities; m_entityMap->updateAllEntities([&](EntityPtr const& entity) { try { entity->update(dt, m_currentStep); } catch (StarException const& e) { if (entity->isMaster()) // this is YOUR problem!! throw e; else // this is THEIR problem!! Logger::error("WorldClient: Exception caught in {}::update ({}): {}", EntityTypeNames.getRight(entity->entityType()), entity->entityId(), e.what()); } 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(std::move(clientPresenceEntities)); m_damageManager->update(dt); handleDamageNotifications(); m_sky->setAltitude(m_clientState.windowCenter()[1]); m_sky->update(dt); RectI particleRegion = m_clientState.window().padded(m_clientConfig.getInt("particleRegionPadding")); m_weather.setVisibleRegion(particleRegion); m_weather.update(dt); 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(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_startupHiddenEntities.erase(itemDrop->entityId()); itemDrop->takeBy(m_mainPlayer->entityId(), (float)m_latency / 1000); m_outgoingPackets.append(make_shared(itemDrop->entityId())); } } } } else { m_requestedDrops.clear(); } sparkDamagedBlocks(); m_particles->addParticles(m_weather.pullNewParticles()); m_particles->update(dt, 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(*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(m_entityUpdateTimer.wrapTick(dt)); if ((!m_clientState.legacy() && m_currentStep % 3 == 0) || m_pingTime.isNothing()) { m_pingTime = Time::monotonicMilliseconds(); m_outgoingPackets.append(make_shared(*m_pingTime)); } LogMap::set("client_ping", m_latency); // Remove active sectors that are outside of the current monitoring region Set neededSectors; auto monitoredRegions = m_clientState.monitoringRegions([this](EntityId entityId) -> Maybe { 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", toString(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, EntityId entityId) { if (!entity) return; if (!inWorld()) return; if (entity->clientEntityMode() != ClientEntityMode::ClientSlaveOnly) { entity->init(this, m_entityMap->reserveEntityId(entityId), EntityMode::Master); m_entityMap->addEntity(entity); notifyEntityCreate(entity); } else { auto entityFactory = Root::singleton().entityFactory(); m_outgoingPackets.append(make_shared(entity->entityType(), entityFactory->netStoreEntity(entity), entity->writeNetState().first)); } } TileDamageResult WorldClient::damageTiles(List const& pos, TileLayer layer, Vec2F const& sourcePosition, TileDamage const& tileDamage, Maybe 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(std::move(toDing), layer, sourcePosition, dingDamage, Maybe())); res = TileDamageResult::Protected; } if (toDamage.size()) { m_outgoingPackets.append(make_shared(std::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 const& tilePositions, LiquidId liquidId) { if (!inWorld()) return; float bucketSize = Root::singleton().assets()->json("/items/defaultParameters.config:liquidItems.bucketSize").toFloat(); float nextUnit = bucketSize; List maybeDrainTiles; for (auto& pos : tilePositions) { if (isTileProtected(pos)) continue; auto& p = m_predictedTiles[pos]; auto const& tile = m_tileArray->tile(pos); if ((p.liquid ? p.liquid->liquid : tile.liquid.liquid) == liquidId) { if (!p.liquid) p.liquid.emplace(tile.liquid.liquid, tile.liquid.level); auto& liquid = *p.liquid; if (liquid.level >= nextUnit) { liquid.take(nextUnit); nextUnit = bucketSize; for (size_t i = 0; i < maybeDrainTiles.size(); ++i) m_predictedTiles[pos].liquid.emplace(EmptyLiquidId, 0.0f); maybeDrainTiles.clear(); } if (liquid.level > 0) { nextUnit -= liquid.level; maybeDrainTiles.append(pos); } } } m_outgoingPackets.append(make_shared(tilePositions, liquidId)); } bool WorldClient::waitForLighting(WorldRenderData* renderData) { MutexLocker prepLocker(m_lightMapPrepMutex); MutexLocker lightMapLocker(m_lightMapMutex); if (renderData && !m_lightMap.empty()) { for (auto& previewTile : m_previewTiles) { if (previewTile.updateLight) { Vec2I lightArrayPos = m_geometry.diff(previewTile.position, m_lightMinPosition); if (lightArrayPos[0] >= 0 && lightArrayPos[0] < (int)m_lightMap.width() && lightArrayPos[1] >= 0 && lightArrayPos[1] < (int)m_lightMap.height()) m_lightMap.set(lightArrayPos[0], lightArrayPos[1], Color::v3bToFloat(previewTile.light)); } } renderData->lightMap = std::move(m_lightMap); renderData->lightMinPosition = m_lightMinPosition; return true; } return false; } WorldClient::BroadcastCallback& WorldClient::broadcastCallback() { return m_broadcastCallback; } 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(bool sendEntityUpdates) { auto& root = Root::singleton(); auto assets = root.assets(); auto entityFactory = root.entityFactory(); m_outgoingPackets.append(make_shared(m_currentTime)); if (m_currentStep % m_clientConfig.getInt("worldClientStateUpdateDelta") == 0) m_outgoingPackets.append(make_shared(m_clientState.writeDelta())); m_entityMap->forAllEntities([&](EntityPtr const& entity) { notifyEntityCreate(entity); }); if (sendEntityUpdates) { auto entityUpdateSet = make_shared(); 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()] = std::move(updateAndVersion.first); *version = updateAndVersion.second; } }); m_outgoingPackets.append(std::move(entityUpdateSet)); } for (auto& remoteHitRequest : m_damageManager->pullRemoteHitRequests()) m_outgoingPackets.append(make_shared(std::move(remoteHitRequest))); for (auto& remoteDamageRequest : m_damageManager->pullRemoteDamageRequests()) m_outgoingPackets.append(make_shared(std::move(remoteDamageRequest))); for (auto& remoteDamageNotification : m_damageManager->pullRemoteDamageNotifications()) m_outgoingPackets.append(make_shared(std::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$", toString(displayValue)); m_particles->add(particle); }; eraseWhere(m_damageNumbers, [&](std::pair 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); const List* directives = nullptr; if (auto& worldTemplate = m_worldTemplate) { if (const auto& parameters = worldTemplate->worldParameters()) if (auto& globalDirectives = m_worldTemplate->worldParameters()->globalDirectives) directives = &globalDirectives.get(); } if (directives) { int directiveIndex = unsigned(damageNotification.targetEntityId) % directives->size(); for (auto& p : hitParticles) p.directives.append(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); } } } } 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 WorldClient::interact(InteractRequest const& request) { if (!inWorld()) return RpcPromise::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(targetEntity); starAssert(interactiveTarget); return RpcPromise::createFulfilled(interactiveTarget->interact(request)); } } auto pair = RpcPromise::createPair(); Uuid requestId; m_entityInteractionResponses[requestId] = pair.second; m_outgoingPackets.append(make_shared(request, requestId)); return pair.first; } void WorldClient::lightingTileGather() { 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. size_t lights = 0; 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; if (tile.foreground != EmptyMaterialId || tile.foregroundMod != NoModId) light += materialDatabase->radiantLight(tile.foreground, tile.foregroundMod); if (tile.liquid.liquid != EmptyLiquidId && tile.liquid.level != 0.0f) light += liquidsDatabase->radiantLight(tile.liquid); if (tile.foregroundLightTransparent) { if (tile.background != EmptyMaterialId || tile.backgroundMod != NoModId) light += materialDatabase->radiantLight(tile.background, tile.backgroundMod); if (tile.backgroundLightTransparent && pos[1] + y > undergroundLevel) light += environmentLight; } if (light.max() > 0.0f) ++lights; m_lightingCalculator.setCellIndex(baseIndex + y, light, !tile.foregroundLightTransparent); } }); LogMap::set("client_render_world_async_light_tiles", toString(lights)); } void WorldClient::lightingCalc() { MutexLocker prepLocker(m_lightMapPrepMutex); RectI lightRange = m_pendingLightRange; List lights = std::move(m_pendingLights); List> particleLights = std::move(m_pendingParticleLights); auto configuration = Root::singleton().configuration(); bool monochrome = configuration->get("monochromeLighting").toBool(); m_lightingCalculator.setMonochrome(monochrome); m_lightingCalculator.begin(lightRange); lightingTileGather(); prepLocker.unlock(); bool useHybridPointLights = configuration->get("newObjectLighting").optBool().value(true); for (auto const& light : lights) { Vec2F position = m_geometry.nearestTo(Vec2F(m_lightingCalculator.calculationRegion().min()), light.position); if (light.type == LightType::Spread) m_lightingCalculator.addSpreadLight(position, light.color); else { if (light.type == LightType::PointAsSpread) { if (!useHybridPointLights) m_lightingCalculator.addSpreadLight(position, light.color); else { // hybrid (used for auto-converted object lights) - 85% spread, 15% point (2nd is applied elsewhere) m_lightingCalculator.addSpreadLight(position, light.color * 0.85f); m_lightingCalculator.addPointLight(position, light.color, light.pointBeam, light.beamAngle, light.beamAmbience, true); } } else // fully additive point light m_lightingCalculator.addPointLight(position, light.color, light.pointBeam, light.beamAngle, light.beamAmbience); } } for (auto const& lightPair : particleLights) { Vec2F position = m_geometry.nearestTo(Vec2F(m_lightingCalculator.calculationRegion().min()), lightPair.first); m_lightingCalculator.addSpreadLight(position, lightPair.second); } m_lightingCalculator.calculate(m_pendingLightMap); { MutexLocker mapLocker(m_lightMapMutex); m_lightMinPosition = lightRange.min(); m_lightMap = std::move(m_pendingLightMap); } } void WorldClient::lightingMain() { MutexLocker condLocker(m_lightingMutex); while (true) { m_lightingCond.wait(m_lightingMutex); if (m_stopLightingThread) return; int64_t start = Time::monotonicMicroseconds(); lightingCalc(); LogMap::set("client_render_world_async_light_calc", strf(u8"{:05d}\u00b5s", Time::monotonicMicroseconds() - start)); } } void WorldClient::initWorld(WorldStartPacket const& startPacket) { clearWorld(); m_outgoingPackets.append(make_shared()); 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_entityUpdateTimer = GameTimer(m_interpolationTracker.entityUpdateDelta()); m_clientId = startPacket.clientId; auto entitySpace = connectionEntitySpace(startPacket.clientId); m_worldTemplate = make_shared(startPacket.templateData); m_entityMap = make_shared(m_worldTemplate->size(), entitySpace.first, entitySpace.second); m_tileArray = make_shared(m_worldTemplate->size()); m_tileGetterFunction = [&, tile = ClientTile()](Vec2I pos) mutable -> ClientTile const& { if (!m_predictedTiles.empty()) { if (auto p = m_predictedTiles.ptr(pos)) { p->apply(tile = m_tileArray->tile(pos)); if (p->liquid) { if (p->liquid->liquid == tile.liquid.liquid) tile.liquid.level += p->liquid->level; else { tile.liquid.liquid = p->liquid->liquid; tile.liquid.level = p->liquid->level; } } return tile; } } return m_tileArray->tile(pos); }; m_damageManager = make_shared(this, startPacket.clientId); m_luaRoot->restart(); 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(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(); 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.getCollision()); }); 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); } } waitForLighting(); m_currentStep = 0; m_currentTime = 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->shutdown(); 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, bool silent) { if (auto spill = m_mainPlayer->pickupItems(item, silent)) addEntity(ItemDrop::createRandomizedDrop(spill->descriptor(), m_mainPlayer->position())); } void WorldClient::notifyEntityCreate(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(entity->entityType(), Root::singleton().entityFactory()->netStoreEntity(entity), std::move(firstNetState.first), entity->entityId())); } } 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(std::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(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, bool updateCollision) { ClientTile* tile = m_tileArray->modifyTile(pos); if (!tile) return false; if (!m_predictedTiles.empty()) { auto findPrediction = m_predictedTiles.find(pos); if (findPrediction != m_predictedTiles.end()) { auto& p = findPrediction->second; if (p.collision && *p.collision == netTile.collision) p.collision.reset(); if (p.foreground && (*p.foreground == StructureMaterialId || *p.foreground == netTile.foreground)) p.foreground.reset(); if (p.foregroundMod && *p.foregroundMod == netTile.foregroundMod) p.foregroundMod.reset(); if (p.foregroundHueShift && *p.foregroundHueShift == netTile.foregroundHueShift) p.foregroundHueShift.reset(); if (p.foregroundModHueShift && *p.foregroundModHueShift == netTile.foregroundModHueShift) p.foregroundModHueShift.reset(); if (p.background && *p.background == netTile.background) p.background.reset(); if (p.backgroundMod && *p.backgroundMod == netTile.backgroundMod) p.backgroundMod.reset(); if (p.backgroundHueShift && *p.backgroundHueShift == netTile.backgroundHueShift) p.backgroundHueShift.reset(); if (p.backgroundModHueShift && *p.backgroundModHueShift == netTile.backgroundModHueShift) p.backgroundModHueShift.reset(); if (!p) m_predictedTiles.erase(findPrediction); } } 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; if (updateCollision) 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(std::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 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(JsonObject{{propertyName, property}})); } bool WorldClient::playerCanReachEntity(EntityId entityId, bool preferInteractive) const { return m_mainPlayer->isAdmin() || canReachEntity(m_mainPlayer->position(), m_mainPlayer->interactRadius(), entityId, preferInteractive); } void WorldClient::disconnectAllWires(Vec2I wireEntityPosition, WireNode const& node) { m_outgoingPackets.append(make_shared(wireEntityPosition, node)); } void WorldClient::connectWire(WireConnection const& output, WireConnection const& input) { m_outgoingPackets.append(make_shared(output, input)); } bool WorldClient::sendSecretBroadcast(StringView broadcast, bool raw, bool compress) { if (!inWorld() || !m_mainPlayer || !m_mainPlayer->getSecretPropertyView(SECRET_BROADCAST_PUBLIC_KEY)) return false; auto signature = Curve25519::sign((void*)broadcast.utf8Ptr(), broadcast.utf8Size()); auto damageNotification = make_shared(); auto& remDmg = damageNotification->remoteDamageNotification; auto& dmg = remDmg.damageNotification; dmg.targetEntityId = dmg.sourceEntityId = remDmg.sourceEntityId = m_mainPlayer->entityId(); dmg.damageDealt = dmg.healthLost = 0.0f; dmg.hitType = HitType::Hit; dmg.damageSourceKind = "nodamage"; dmg.targetMaterialKind = raw ? broadcast : strf("{}{}{}", SECRET_BROADCAST_PREFIX, StringView((char*)&signature, sizeof(signature)), broadcast); dmg.position = m_mainPlayer->position(); if (!compress) damageNotification->setCompressionMode(PacketCompressionMode::Disabled); m_outgoingPackets.emplace_back(std::move(damageNotification)); return true; } bool WorldClient::handleSecretBroadcast(PlayerPtr player, StringView broadcast) { if (m_broadcastCallback) return m_broadcastCallback(player, broadcast); else return false; } void WorldClient::ClientRenderCallback::addDrawable(Drawable drawable, EntityRenderLayer renderLayer) { drawables[renderLayer].append(std::move(drawable)); } void WorldClient::ClientRenderCallback::addLightSource(LightSource lightSource) { lightSources.append(std::move(lightSource)); } void WorldClient::ClientRenderCallback::addParticle(Particle particle) { particles.append(std::move(particle)); } void WorldClient::ClientRenderCallback::addAudio(AudioInstancePtr audio) { audios.append(std::move(audio)); } void WorldClient::ClientRenderCallback::addTilePreview(PreviewTile preview) { previewTiles.append(std::move(preview)); } void WorldClient::ClientRenderCallback::addOverheadBar(OverheadBar bar) { overheadBars.append(std::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 WorldClient::findUniqueEntity(String const& uniqueId) { if (auto entity = m_entityMap->uniqueEntity(uniqueId)) return RpcPromise::createFulfilled(entity->position()); auto pair = RpcPromise::createPair(); auto& rpcPromises = m_findUniqueEntityResponses[uniqueId]; if (rpcPromises.empty()) m_outgoingPackets.append(make_shared(uniqueId)); rpcPromises.append(pair.second); return pair.first; } RpcPromise WorldClient::sendEntityMessage(Variant const& entityId, String const& message, JsonArray const& args) { EntityPtr entity; if (entityId.is()) entity = m_entityMap->entity(entityId.get()); else entity = m_entityMap->uniqueEntity(entityId.get()); // 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() && !entity && m_clientId == connectionForEntity(entityId.get())) { return RpcPromise::createFailed("Unknown entity"); } else if (entity && entity->isMaster()) { if (auto resp = entity->receiveMessage(*m_clientId, message, args)) return RpcPromise::createFulfilled(resp.take()); else return RpcPromise::createFailed("Message not handled by entity"); } else { auto pair = RpcPromise::createPair(); Uuid uuid; m_entityMessageResponses[uuid] = pair.second; m_outgoingPackets.append(make_shared(entityId, message, args, uuid)); return pair.first; } } List WorldClient::pullPendingChatActions() { List result; if (m_entityMap) { for (auto const& entity : m_entityMap->all()) 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(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(RectF(clientWindow))) { for (auto const& forceRegion : physics->forceRegions()) { if (auto dfr = forceRegion.ptr()) logPoly(dfr->region, {}, 1.0f, 1.0f, 0.0f); else if (auto rfr = forceRegion.ptr()) 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::informTilePrediction(Vec2I const& pos, TileModification const& modification) { auto now = Time::monotonicMilliseconds(); auto& p = m_predictedTiles[pos]; p.time = now; if (auto placeMaterial = modification.ptr()) { if (placeMaterial->layer == TileLayer::Foreground) { auto materialDatabase = Root::singleton().materialDatabase(); if (!materialDatabase->isCascadingFallingMaterial(placeMaterial->material) && !materialDatabase-> isFallingMaterial(placeMaterial->material)) { p.foreground = placeMaterial->material; p.foregroundHueShift = placeMaterial->materialHueShift; } else p.foreground = StructureMaterialId; if (placeMaterial->collisionOverride != TileCollisionOverride::None) p.collision = collisionKindFromOverride(placeMaterial->collisionOverride); else p.collision = materialDatabase->materialCollisionKind(placeMaterial->material); dirtyCollision(RectI::withSize(pos, { 1, 1 })); } else { p.background = placeMaterial->material; p.backgroundHueShift = placeMaterial->materialHueShift; } } else if (auto placeMod = modification.ptr()) { if (placeMod->layer == TileLayer::Foreground) p.foregroundMod = placeMod->mod; else p.backgroundMod = placeMod->mod; } else if (auto placeColor = modification.ptr()) { if (placeColor->layer == TileLayer::Foreground) p.foregroundColorVariant = placeColor->color; else p.backgroundColorVariant = placeColor->color; } else if (auto placeLiquid = modification.ptr()) { if (!p.liquid || p.liquid->liquid != placeLiquid->liquid) p.liquid = LiquidLevel(placeLiquid->liquid, placeLiquid->liquidLevel); else p.liquid->level += placeLiquid->liquidLevel; } } 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); } } }