#include "weather.hpp" #include #include #include #include #include /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include #include "../mwmp/Main.hpp" #include "../mwmp/Networking.hpp" #include "../mwmp/Worldstate.hpp" /* End of tes3mp addition */ #include "../mwbase/environment.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwsound/sound.hpp" #include "../mwrender/renderingmanager.hpp" #include "../mwrender/sky.hpp" #include "player.hpp" #include "esmstore.hpp" #include "cellstore.hpp" #include using namespace MWWorld; namespace { static const int invalidWeatherID = -1; // linear interpolate between x and y based on factor. float lerp (float x, float y, float factor) { return x * (1-factor) + y * factor; } // linear interpolate between x and y based on factor. osg::Vec4f lerp (const osg::Vec4f& x, const osg::Vec4f& y, float factor) { return x * (1-factor) + y * factor; } } template T TimeOfDayInterpolator::getValue(const float gameHour, const TimeOfDaySettings& timeSettings, const std::string& prefix) const { WeatherSetting setting = timeSettings.getSetting(prefix); float preSunriseTime = setting.mPreSunriseTime; float postSunriseTime = setting.mPostSunriseTime; float preSunsetTime = setting.mPreSunsetTime; float postSunsetTime = setting.mPostSunsetTime; // night if (gameHour < timeSettings.mNightEnd - preSunriseTime || gameHour > timeSettings.mNightStart + postSunsetTime) return mNightValue; // sunrise else if (gameHour >= timeSettings.mNightEnd - preSunriseTime && gameHour <= timeSettings.mDayStart + postSunriseTime) { float duration = timeSettings.mDayStart + postSunriseTime - timeSettings.mNightEnd + preSunriseTime; float middle = timeSettings.mNightEnd - preSunriseTime + duration / 2.f; if (gameHour <= middle) { // fade in float advance = middle - gameHour; float factor = 0.f; if (duration > 0) factor = advance / duration * 2; return lerp(mSunriseValue, mNightValue, factor); } else { // fade out float advance = gameHour - middle; float factor = 1.f; if (duration > 0) factor = advance / duration * 2; return lerp(mSunriseValue, mDayValue, factor); } } // day else if (gameHour > timeSettings.mDayStart + postSunriseTime && gameHour < timeSettings.mDayEnd - preSunsetTime) return mDayValue; // sunset else if (gameHour >= timeSettings.mDayEnd - preSunsetTime && gameHour <= timeSettings.mNightStart + postSunsetTime) { float duration = timeSettings.mNightStart + postSunsetTime - timeSettings.mDayEnd + preSunsetTime; float middle = timeSettings.mDayEnd - preSunsetTime + duration / 2.f; if (gameHour <= middle) { // fade in float advance = middle - gameHour; float factor = 0.f; if (duration > 0) factor = advance / duration * 2; return lerp(mSunsetValue, mDayValue, factor); } else { // fade out float advance = gameHour - middle; float factor = 1.f; if (duration > 0) factor = advance / duration * 2; return lerp(mSunsetValue, mNightValue, factor); } } // shut up compiler return T(); } template class MWWorld::TimeOfDayInterpolator; template class MWWorld::TimeOfDayInterpolator; Weather::Weather(const std::string& name, const Fallback::Map& fallback, float stormWindSpeed, float rainSpeed, float dlFactor, float dlOffset, const std::string& particleEffect) : mCloudTexture(fallback.getFallbackString("Weather_" + name + "_Cloud_Texture")) , mSkyColor(fallback.getFallbackColour("Weather_" + name +"_Sky_Sunrise_Color"), fallback.getFallbackColour("Weather_" + name + "_Sky_Day_Color"), fallback.getFallbackColour("Weather_" + name + "_Sky_Sunset_Color"), fallback.getFallbackColour("Weather_" + name + "_Sky_Night_Color")) , mFogColor(fallback.getFallbackColour("Weather_" + name + "_Fog_Sunrise_Color"), fallback.getFallbackColour("Weather_" + name + "_Fog_Day_Color"), fallback.getFallbackColour("Weather_" + name + "_Fog_Sunset_Color"), fallback.getFallbackColour("Weather_" + name + "_Fog_Night_Color")) , mAmbientColor(fallback.getFallbackColour("Weather_" + name + "_Ambient_Sunrise_Color"), fallback.getFallbackColour("Weather_" + name + "_Ambient_Day_Color"), fallback.getFallbackColour("Weather_" + name + "_Ambient_Sunset_Color"), fallback.getFallbackColour("Weather_" + name + "_Ambient_Night_Color")) , mSunColor(fallback.getFallbackColour("Weather_" + name + "_Sun_Sunrise_Color"), fallback.getFallbackColour("Weather_" + name + "_Sun_Day_Color"), fallback.getFallbackColour("Weather_" + name + "_Sun_Sunset_Color"), fallback.getFallbackColour("Weather_" + name + "_Sun_Night_Color")) , mLandFogDepth(fallback.getFallbackFloat("Weather_" + name + "_Land_Fog_Day_Depth"), fallback.getFallbackFloat("Weather_" + name + "_Land_Fog_Day_Depth"), fallback.getFallbackFloat("Weather_" + name + "_Land_Fog_Day_Depth"), fallback.getFallbackFloat("Weather_" + name + "_Land_Fog_Night_Depth")) , mSunDiscSunsetColor(fallback.getFallbackColour("Weather_" + name + "_Sun_Disc_Sunset_Color")) , mWindSpeed(fallback.getFallbackFloat("Weather_" + name + "_Wind_Speed")) , mCloudSpeed(fallback.getFallbackFloat("Weather_" + name + "_Cloud_Speed")) , mGlareView(fallback.getFallbackFloat("Weather_" + name + "_Glare_View")) , mIsStorm(mWindSpeed > stormWindSpeed) , mRainSpeed(rainSpeed) , mRainFrequency(fallback.getFallbackFloat("Weather_" + name + "_Rain_Entrance_Speed")) , mParticleEffect(particleEffect) , mRainEffect(fallback.getFallbackBool("Weather_" + name + "_Using_Precip") ? "meshes\\raindrop.nif" : "") , mTransitionDelta(fallback.getFallbackFloat("Weather_" + name + "_Transition_Delta")) , mCloudsMaximumPercent(fallback.getFallbackFloat("Weather_" + name + "_Clouds_Maximum_Percent")) , mThunderFrequency(fallback.getFallbackFloat("Weather_" + name + "_Thunder_Frequency")) , mThunderThreshold(fallback.getFallbackFloat("Weather_" + name + "_Thunder_Threshold")) , mThunderSoundID() , mFlashDecrement(fallback.getFallbackFloat("Weather_" + name + "_Flash_Decrement")) , mFlashBrightness(0.0f) { mDL.FogFactor = dlFactor; mDL.FogOffset = dlOffset; mThunderSoundID[0] = fallback.getFallbackString("Weather_" + name + "_Thunder_Sound_ID_0"); mThunderSoundID[1] = fallback.getFallbackString("Weather_" + name + "_Thunder_Sound_ID_1"); mThunderSoundID[2] = fallback.getFallbackString("Weather_" + name + "_Thunder_Sound_ID_2"); mThunderSoundID[3] = fallback.getFallbackString("Weather_" + name + "_Thunder_Sound_ID_3"); // TODO: support weathers that have both "Ambient Loop Sound ID" and "Rain Loop Sound ID", need to play both sounds at the same time. if (!mRainEffect.empty()) // NOTE: in vanilla, the weathers with rain seem to be hardcoded; changing Using_Precip has no effect { mAmbientLoopSoundID = fallback.getFallbackString("Weather_" + name + "_Rain_Loop_Sound_ID"); if (mAmbientLoopSoundID.empty()) // default to "rain" if not set mAmbientLoopSoundID = "rain"; } else mAmbientLoopSoundID = fallback.getFallbackString("Weather_" + name + "_Ambient_Loop_Sound_ID"); if (Misc::StringUtils::ciEqual(mAmbientLoopSoundID, "None")) mAmbientLoopSoundID.clear(); /* Unhandled: Rain Diameter=600 ? Rain Height Min=200 ? Rain Height Max=700 ? Rain Threshold=0.6 ? Max Raindrops=650 ? */ } float Weather::transitionDelta() const { // Transition Delta describes how quickly transitioning to the weather in question will take, in Hz. Note that the // measurement is in real time, not in-game time. return mTransitionDelta; } float Weather::cloudBlendFactor(const float transitionRatio) const { // Clouds Maximum Percent affects how quickly the sky transitions from one sky texture to the next. return transitionRatio / mCloudsMaximumPercent; } float Weather::calculateThunder(const float transitionRatio, const float elapsedSeconds, const bool isPaused) { // When paused, the flash brightness remains the same and no new strikes can occur. if(!isPaused) { // Morrowind doesn't appear to do any calculations unless the transition ratio is higher than the Thunder Threshold. if(transitionRatio >= mThunderThreshold && mThunderFrequency > 0.0f) { flashDecrement(elapsedSeconds); if(Misc::Rng::rollProbability() <= thunderChance(transitionRatio, elapsedSeconds)) { lightningAndThunder(); } } else { mFlashBrightness = 0.0f; } } return mFlashBrightness; } inline void Weather::flashDecrement(const float elapsedSeconds) { // The Flash Decrement is measured in whole units per second. This means that if the flash brightness was // currently 1.0, then it should take approximately 0.25 seconds to decay to 0.0 (the minimum). float decrement = mFlashDecrement * elapsedSeconds; mFlashBrightness = decrement > mFlashBrightness ? 0.0f : mFlashBrightness - decrement; } inline float Weather::thunderChance(const float transitionRatio, const float elapsedSeconds) const { // This formula is reversed from the observation that with Thunder Frequency set to 1, there are roughly 10 strikes // per minute. It doesn't appear to be tied to in game time as Timescale doesn't affect it. Various values of // Thunder Frequency seem to change the average number of strikes in a linear fashion.. During a transition, it appears to // scaled based on how far past it is past the Thunder Threshold. float scaleFactor = (transitionRatio - mThunderThreshold) / (1.0f - mThunderThreshold); return ((mThunderFrequency * 10.0f) / 60.0f) * elapsedSeconds * scaleFactor; } inline void Weather::lightningAndThunder(void) { // Morrowind seems to vary the intensity of the brightness based on which of the four sound IDs it selects. // They appear to go from 0 (brightest, closest) to 3 (faintest, farthest). The value of 0.25 per distance // was derived by setting the Flash Decrement to 0.1 and measuring how long each value took to decay to 0. // TODO: Determine the distribution of each distance to see if it's evenly weighted. unsigned int distance = Misc::Rng::rollDice(4); // Flash brightness appears additive, since if multiple strikes occur, it takes longer for it to decay to 0. mFlashBrightness += 1 - (distance * 0.25f); MWBase::Environment::get().getSoundManager()->playSound(mThunderSoundID[distance], 1.0, 1.0); } RegionWeather::RegionWeather(const ESM::Region& region) : mWeather(invalidWeatherID) , mChances() { mChances.reserve(10); mChances.push_back(region.mData.mClear); mChances.push_back(region.mData.mCloudy); mChances.push_back(region.mData.mFoggy); mChances.push_back(region.mData.mOvercast); mChances.push_back(region.mData.mRain); mChances.push_back(region.mData.mThunder); mChances.push_back(region.mData.mAsh); mChances.push_back(region.mData.mBlight); mChances.push_back(region.mData.mA); mChances.push_back(region.mData.mB); } RegionWeather::RegionWeather(const ESM::RegionWeatherState& state) : mWeather(state.mWeather) , mChances(state.mChances) { } RegionWeather::operator ESM::RegionWeatherState() const { ESM::RegionWeatherState state = { mWeather, mChances }; return state; } void RegionWeather::setChances(const std::vector& chances) { if(mChances.size() < chances.size()) { mChances.reserve(chances.size()); } std::vector::const_iterator it = chances.begin(); for(size_t i = 0; it != chances.end(); ++it, ++i) { mChances[i] = *it; } // Regional weather no longer supports the current type, select a new weather pattern. if((static_cast(mWeather) >= mChances.size()) || (mChances[mWeather] == 0)) { chooseNewWeather(); } } void RegionWeather::setWeather(int weatherID) { mWeather = weatherID; } int RegionWeather::getWeather() { // If the region weather was already set (by ChangeWeather, or by a previous call) then just return that value. // Note that the region weather will be expired periodically when the weather update timer expires. if(mWeather == invalidWeatherID) { chooseNewWeather(); } return mWeather; } void RegionWeather::chooseNewWeather() { // All probabilities must add to 100 (responsibility of the user). // If chances A and B has values 30 and 70 then by generating 100 numbers 1..100, 30% will be lesser or equal 30 // and 70% will be greater than 30 (in theory). int chance = Misc::Rng::rollDice(100) + 1; // 1..100 int sum = 0; int i = 0; for(; static_cast(i) < mChances.size(); ++i) { sum += mChances[i]; if(chance <= sum) { mWeather = i; return; } } // if we hit this path then the chances don't add to 100, choose a default weather instead mWeather = 0; } MoonModel::MoonModel(const std::string& name, const Fallback::Map& fallback) : mFadeInStart(fallback.getFallbackFloat("Moons_" + name + "_Fade_In_Start")) , mFadeInFinish(fallback.getFallbackFloat("Moons_" + name + "_Fade_In_Finish")) , mFadeOutStart(fallback.getFallbackFloat("Moons_" + name + "_Fade_Out_Start")) , mFadeOutFinish(fallback.getFallbackFloat("Moons_" + name + "_Fade_Out_Finish")) , mAxisOffset(fallback.getFallbackFloat("Moons_" + name + "_Axis_Offset")) , mSpeed(fallback.getFallbackFloat("Moons_" + name + "_Speed")) , mDailyIncrement(fallback.getFallbackFloat("Moons_" + name + "_Daily_Increment")) , mFadeStartAngle(fallback.getFallbackFloat("Moons_" + name + "_Fade_Start_Angle")) , mFadeEndAngle(fallback.getFallbackFloat("Moons_" + name + "_Fade_End_Angle")) , mMoonShadowEarlyFadeAngle(fallback.getFallbackFloat("Moons_" + name + "_Moon_Shadow_Early_Fade_Angle")) { // Morrowind appears to have a minimum speed in order to avoid situations where the moon couldn't conceivably // complete a rotation in a single 24 hour period. The value of 180/23 was deduced from reverse engineering. mSpeed = std::min(mSpeed, 180.0f / 23.0f); } MWRender::MoonState MoonModel::calculateState(const TimeStamp& gameTime) const { float rotationFromHorizon = angle(gameTime); MWRender::MoonState state = { rotationFromHorizon, mAxisOffset, // Reverse engineered from Morrowind's scene graph rotation matrices. static_cast(phase(gameTime)), shadowBlend(rotationFromHorizon), earlyMoonShadowAlpha(rotationFromHorizon) * hourlyAlpha(gameTime.getHour()) }; return state; } inline float MoonModel::angle(const TimeStamp& gameTime) const { // Morrowind's moons start travel on one side of the horizon (let's call it H-rise) and travel 180 degrees to the // opposite horizon (let's call it H-set). Upon reaching H-set, they reset to H-rise until the next moon rise. // When calculating the angle of the moon, several cases have to be taken into account: // 1. Moon rises and then sets in one day. // 2. Moon sets and doesn't rise in one day (occurs when the moon rise hour is >= 24). // 3. Moon sets and then rises in one day. float moonRiseHourToday = moonRiseHour(gameTime.getDay()); float moonRiseAngleToday = 0; if(gameTime.getHour() < moonRiseHourToday) { float moonRiseHourYesterday = moonRiseHour(gameTime.getDay() - 1); if(moonRiseHourYesterday < 24) { float moonRiseAngleYesterday = rotation(24 - moonRiseHourYesterday); if(moonRiseAngleYesterday < 180) { // The moon rose but did not set yesterday, so accumulate yesterday's angle with how much we've travelled today. moonRiseAngleToday = rotation(gameTime.getHour()) + moonRiseAngleYesterday; } } } else { moonRiseAngleToday = rotation(gameTime.getHour() - moonRiseHourToday); } if(moonRiseAngleToday >= 180) { // The moon set today, reset the angle to the horizon. moonRiseAngleToday = 0; } return moonRiseAngleToday; } inline float MoonModel::moonRiseHour(unsigned int daysPassed) const { // This arises from the start date of 16 Last Seed, 427 // TODO: Find an alternate formula that doesn't rely on this day being fixed. static const unsigned int startDay = 16; // This odd formula arises from the fact that on 16 Last Seed, 17 increments have occurred, meaning // that upon starting a new game, it must only calculate the moon phase as far back as 1 Last Seed. // Note that we don't modulo after adding the latest daily increment because other calculations need to // know if doing so would cause the moon rise to be postponed until the next day (which happens when // the moon rise hour is >= 24 in Morrowind). return mDailyIncrement + std::fmod((daysPassed - 1 + startDay) * mDailyIncrement, 24.0f); } inline float MoonModel::rotation(float hours) const { // 15 degrees per hour was reverse engineered from the rotation matrices of the Morrowind scene graph. // Note that this correlates to 360 / 24, which is a full rotation every 24 hours, so speed is a measure // of whole rotations that could be completed in a day. return 15.0f * mSpeed * hours; } inline unsigned int MoonModel::phase(const TimeStamp& gameTime) const { // Morrowind starts with a full moon on 16 Last Seed and then begins to wane 17 Last Seed, working on 3 day phase cycle. // Note: this is an internal helper, and as such we don't want to return MWRender::MoonState::Phase since we can't // forward declare it (C++11 strongly typed enums solve this). // If the moon didn't rise yet today, use yesterday's moon phase. if(gameTime.getHour() < moonRiseHour(gameTime.getDay())) return (gameTime.getDay() / 3) % 8; else return ((gameTime.getDay() + 1) / 3) % 8; } inline float MoonModel::shadowBlend(float angle) const { // The Fade End Angle and Fade Start Angle describe a region where the moon transitions from a solid disk // that is roughly the color of the sky, to a textured surface. // Depending on the current angle, the following values describe the ratio between the textured moon // and the solid disk: // 1. From Fade End Angle 1 to Fade Start Angle 1 (during moon rise): 0..1 // 2. From Fade Start Angle 1 to Fade Start Angle 2 (between moon rise and moon set): 1 (textured) // 3. From Fade Start Angle 2 to Fade End Angle 2 (during moon set): 1..0 // 4. From Fade End Angle 2 to Fade End Angle 1 (between moon set and moon rise): 0 (solid disk) float fadeAngle = mFadeStartAngle - mFadeEndAngle; float fadeEndAngle2 = 180.0f - mFadeEndAngle; float fadeStartAngle2 = 180.0f - mFadeStartAngle; if((angle >= mFadeEndAngle) && (angle < mFadeStartAngle)) return (angle - mFadeEndAngle) / fadeAngle; else if((angle >= mFadeStartAngle) && (angle < fadeStartAngle2)) return 1.0f; else if((angle >= fadeStartAngle2) && (angle < fadeEndAngle2)) return (fadeEndAngle2 - angle) / fadeAngle; else return 0.0f; } inline float MoonModel::hourlyAlpha(float gameHour) const { // The Fade Out Start / Finish and Fade In Start / Finish describe the hours at which the moon // appears and disappears. // Depending on the current hour, the following values describe how transparent the moon is. // 1. From Fade Out Start to Fade Out Finish: 1..0 // 2. From Fade Out Finish to Fade In Start: 0 (transparent) // 3. From Fade In Start to Fade In Finish: 0..1 // 4. From Fade In Finish to Fade Out Start: 1 (solid) if((gameHour >= mFadeOutStart) && (gameHour < mFadeOutFinish)) return (mFadeOutFinish - gameHour) / (mFadeOutFinish - mFadeOutStart); else if((gameHour >= mFadeOutFinish) && (gameHour < mFadeInStart)) return 0.0f; else if((gameHour >= mFadeInStart) && (gameHour < mFadeInFinish)) return (gameHour - mFadeInStart) / (mFadeInFinish - mFadeInStart); else return 1.0f; } inline float MoonModel::earlyMoonShadowAlpha(float angle) const { // The Moon Shadow Early Fade Angle describes an arc relative to Fade End Angle. // Depending on the current angle, the following values describe how transparent the moon is. // 1. From Moon Shadow Early Fade Angle 1 to Fade End Angle 1 (during moon rise): 0..1 // 2. From Fade End Angle 1 to Fade End Angle 2 (between moon rise and moon set): 1 (solid) // 3. From Fade End Angle 2 to Moon Shadow Early Fade Angle 2 (during moon set): 1..0 // 4. From Moon Shadow Early Fade Angle 2 to Moon Shadow Early Fade Angle 1: 0 (transparent) float moonShadowEarlyFadeAngle1 = mFadeEndAngle - mMoonShadowEarlyFadeAngle; float fadeEndAngle2 = 180.0f - mFadeEndAngle; float moonShadowEarlyFadeAngle2 = fadeEndAngle2 + mMoonShadowEarlyFadeAngle; if((angle >= moonShadowEarlyFadeAngle1) && (angle < mFadeEndAngle)) return (angle - moonShadowEarlyFadeAngle1) / mMoonShadowEarlyFadeAngle; else if((angle >= mFadeEndAngle) && (angle < fadeEndAngle2)) return 1.0f; else if((angle >= fadeEndAngle2) && (angle < moonShadowEarlyFadeAngle2)) return (moonShadowEarlyFadeAngle2 - angle) / mMoonShadowEarlyFadeAngle; else return 0.0f; } WeatherManager::WeatherManager(MWRender::RenderingManager& rendering, const Fallback::Map& fallback, MWWorld::ESMStore& store) : mStore(store) , mRendering(rendering) , mSunriseTime(fallback.getFallbackFloat("Weather_Sunrise_Time")) , mSunsetTime(fallback.getFallbackFloat("Weather_Sunset_Time")) , mSunriseDuration(fallback.getFallbackFloat("Weather_Sunrise_Duration")) , mSunsetDuration(fallback.getFallbackFloat("Weather_Sunset_Duration")) , mSunPreSunsetTime(fallback.getFallbackFloat("Weather_Sun_Pre-Sunset_Time")) , mNightFade(0, 0, 0, 1) , mHoursBetweenWeatherChanges(fallback.getFallbackFloat("Weather_Hours_Between_Weather_Changes")) , mRainSpeed(fallback.getFallbackFloat("Weather_Precip_Gravity")) , mUnderwaterFog(fallback.getFallbackFloat("Water_UnderwaterSunriseFog"), fallback.getFallbackFloat("Water_UnderwaterDayFog"), fallback.getFallbackFloat("Water_UnderwaterSunsetFog"), fallback.getFallbackFloat("Water_UnderwaterNightFog")) , mWeatherSettings() , mMasser("Masser", fallback) , mSecunda("Secunda", fallback) , mWindSpeed(0.f) , mIsStorm(false) , mPrecipitation(false) , mStormDirection(0,1,0) , mCurrentRegion() , mTimePassed(0) , mFastForward(false) , mWeatherUpdateTime(mHoursBetweenWeatherChanges) , mTransitionFactor(0) , mCurrentWeather(0) , mNextWeather(0) , mQueuedWeather(0) , mRegions() , mResult() , mAmbientSound(nullptr) , mPlayingSoundID() { mTimeSettings.mNightStart = mSunsetTime + mSunsetDuration; mTimeSettings.mNightEnd = mSunriseTime; mTimeSettings.mDayStart = mSunriseTime + mSunriseDuration; mTimeSettings.mDayEnd = mSunsetTime; mTimeSettings.addSetting(fallback, "Sky"); mTimeSettings.addSetting(fallback, "Ambient"); mTimeSettings.addSetting(fallback, "Fog"); mTimeSettings.addSetting(fallback, "Sun"); // Morrowind handles stars settings differently for other ones mTimeSettings.mStarsPostSunsetStart = fallback.getFallbackFloat("Weather_Stars_Post-Sunset_Start"); mTimeSettings.mStarsPreSunriseFinish = fallback.getFallbackFloat("Weather_Stars_Pre-Sunrise_Finish"); mTimeSettings.mStarsFadingDuration = fallback.getFallbackFloat("Weather_Stars_Fading_Duration"); WeatherSetting starSetting = { mTimeSettings.mStarsPreSunriseFinish, mTimeSettings.mStarsFadingDuration - mTimeSettings.mStarsPreSunriseFinish, mTimeSettings.mStarsPostSunsetStart, mTimeSettings.mStarsFadingDuration - mTimeSettings.mStarsPostSunsetStart }; mTimeSettings.mSunriseTransitions["Stars"] = starSetting; mWeatherSettings.reserve(10); // These distant land fog factor and offset values are the defaults MGE XE provides. Should be // provided by settings somewhere? addWeather("Clear", fallback, 1.0f, 0.0f); // 0 addWeather("Cloudy", fallback, 0.9f, 0.0f); // 1 addWeather("Foggy", fallback, 0.2f, 30.0f); // 2 addWeather("Overcast", fallback, 0.7f, 0.0f); // 3 addWeather("Rain", fallback, 0.5f, 10.0f); // 4 addWeather("Thunderstorm", fallback, 0.5f, 20.0f); // 5 addWeather("Ashstorm", fallback, 0.2f, 50.0f, "meshes\\ashcloud.nif"); // 6 addWeather("Blight", fallback, 0.2f, 60.0f, "meshes\\blightcloud.nif"); // 7 addWeather("Snow", fallback, 0.5f, 40.0f, "meshes\\snow.nif"); // 8 addWeather("Blizzard", fallback, 0.16f, 70.0f, "meshes\\blizzard.nif"); // 9 Store::iterator it = store.get().begin(); for(; it != store.get().end(); ++it) { std::string regionID = Misc::StringUtils::lowerCase(it->mId); mRegions.insert(std::make_pair(regionID, RegionWeather(*it))); } forceWeather(0); } WeatherManager::~WeatherManager() { stopSounds(); } void WeatherManager::changeWeather(const std::string& regionID, const unsigned int weatherID) { // In Morrowind, this seems to have the following behavior, when applied to the current region: // - When there is no transition in progress, start transitioning to the new weather. // - If there is a transition in progress, queue up the transition and process it when the current one completes. // - If there is a transition in progress, and a queued transition, overwrite the queued transition. // - If multiple calls to ChangeWeather are made while paused (console up), only the last call will be used, // meaning that if there was no transition in progress, only the last ChangeWeather will be processed. // If the region isn't current, Morrowind will store the new weather for the region in question. if(weatherID < mWeatherSettings.size()) { std::string lowerCaseRegionID = Misc::StringUtils::lowerCase(regionID); std::map::iterator it = mRegions.find(lowerCaseRegionID); if(it != mRegions.end()) { it->second.setWeather(weatherID); regionalWeatherChanged(it->first, it->second); } } } void WeatherManager::modRegion(const std::string& regionID, const std::vector& chances) { // Sets the region's probability for various weather patterns. Note that this appears to be saved permanently. // In Morrowind, this seems to have the following behavior when applied to the current region: // - If the region supports the current weather, no change in current weather occurs. // - If the region no longer supports the current weather, and there is no transition in progress, begin to // transition to a new supported weather type. // - If the region no longer supports the current weather, and there is a transition in progress, queue a // transition to a new supported weather type. std::string lowerCaseRegionID = Misc::StringUtils::lowerCase(regionID); std::map::iterator it = mRegions.find(lowerCaseRegionID); if(it != mRegions.end()) { it->second.setChances(chances); regionalWeatherChanged(it->first, it->second); } } void WeatherManager::playerTeleported(const std::string& playerRegion, bool isExterior) { // If the player teleports to an outdoors cell in a new region (for instance, by travelling), the weather needs to // be changed immediately, and any transitions for the previous region discarded. { std::map::iterator it = mRegions.find(playerRegion); if(it != mRegions.end() && playerRegion != mCurrentRegion) { /* Start of tes3mp addition If we've moved to another region, set our weather creation ability to false; the server will set it to true if it wants us creating weather here */ setWeatherCreationState(false); /* End of tes3mp addition */ mCurrentRegion = playerRegion; forceWeather(it->second.getWeather()); } /* Start of tes3mp addition There's no scenario where we want our weather creation ability to be true in an interior, so set it to false */ else if (!isExterior) setWeatherCreationState(false); /* End of tes3mp addition */ } } /* Start of tes3mp addition Make it possible to set a specific weather state for a region from elsewhere in the code */ void WeatherManager::setRegionWeather(const std::string& region, const unsigned int currentWeather, const unsigned int nextWeather, const unsigned int queuedWeather, const float transitionFactor, bool force) { bool isSameRegion = Misc::StringUtils::ciEqual(region, mCurrentRegion); // Only ever force weather if we are in the correct region for it if (isSameRegion) { if (force) { mCurrentWeather = currentWeather; mNextWeather = nextWeather; mQueuedWeather = queuedWeather; mTransitionFactor = transitionFactor; } else { // Keep the queued weather in sync if everything else already is if (mCurrentWeather == currentWeather && mNextWeather == nextWeather) { mQueuedWeather = queuedWeather; } // Start moving towards the next weather immediately if it's different from the one we have else if (mNextWeather != nextWeather && nextWeather != -1) { changeWeather(region, nextWeather); } // Otherwise, if the current weather is different from the one we should have, move towards it else if (mCurrentWeather != currentWeather) { changeWeather(region, currentWeather); } } } else { changeWeather(region, currentWeather); } } /* End of tes3mp addition */ void WeatherManager::update(float duration, bool paused, const TimeStamp& time, bool isExterior) { MWWorld::ConstPtr player = MWMechanics::getPlayer(); if(!paused || mFastForward) { // Add new transitions when either the player's current external region changes. std::string playerRegion = Misc::StringUtils::lowerCase(player.getCell()->getCell()->mRegion); if(updateWeatherTime() || updateWeatherRegion(playerRegion)) { std::map::iterator it = mRegions.find(mCurrentRegion); if(it != mRegions.end()) { addWeatherTransition(it->second.getWeather()); } } updateWeatherTransitions(duration); } if(!isExterior) { mRendering.setSkyEnabled(false); stopSounds(); return; } calculateWeatherResult(time.getHour(), duration, paused); mWindSpeed = mResult.mWindSpeed; mIsStorm = mResult.mIsStorm; // For some reason Ash Storm is not considered as a precipitation weather in game mPrecipitation = !(mResult.mParticleEffect.empty() && mResult.mRainEffect.empty()) && mResult.mParticleEffect != "meshes\\ashcloud.nif"; if (mIsStorm) { osg::Vec3f playerPos (player.getRefData().getPosition().asVec3()); osg::Vec3f redMountainPos (19950, 72032, 27831); mStormDirection = (playerPos - redMountainPos); mStormDirection.z() = 0; mStormDirection.normalize(); mRendering.getSkyManager()->setStormDirection(mStormDirection); } // disable sun during night if (time.getHour() >= mTimeSettings.mNightStart || time.getHour() <= mSunriseTime) mRendering.getSkyManager()->sunDisable(); else mRendering.getSkyManager()->sunEnable(); // Update the sun direction. Run it east to west at a fixed angle from overhead. // The sun's speed at day and night may differ, since mSunriseTime and mNightStart // mark when the sun is level with the horizon. { // Shift times into a 24-hour window beginning at mSunriseTime... float adjustedHour = time.getHour(); float adjustedNightStart = mTimeSettings.mNightStart; if ( time.getHour() < mSunriseTime ) adjustedHour += 24.f; if ( mTimeSettings.mNightStart < mSunriseTime ) adjustedNightStart += 24.f; const bool is_night = adjustedHour >= adjustedNightStart; const float dayDuration = adjustedNightStart - mSunriseTime; const float nightDuration = 24.f - dayDuration; double theta; if ( !is_night ) { theta = static_cast(osg::PI) * (adjustedHour - mSunriseTime) / dayDuration; } else { theta = static_cast(osg::PI) + static_cast(osg::PI) * (adjustedHour - adjustedNightStart) / nightDuration; } osg::Vec3f final( static_cast(cos(theta)), -0.268f, // approx tan( -15 degrees ) static_cast(sin(theta))); mRendering.setSunDirection( final * -1 ); } float underwaterFog = mUnderwaterFog.getValue(time.getHour(), mTimeSettings, "Fog"); float peakHour = mSunriseTime + (mSunsetTime - mSunriseTime) / 2; if (time.getHour() < mSunriseTime || time.getHour() > mSunsetTime) mRendering.getSkyManager()->setGlareTimeOfDayFade(0); else if (time.getHour() < peakHour) mRendering.getSkyManager()->setGlareTimeOfDayFade(1 - (peakHour - time.getHour()) / (peakHour - mSunriseTime)); else mRendering.getSkyManager()->setGlareTimeOfDayFade(1 - (time.getHour() - peakHour) / (mSunsetTime - peakHour)); mRendering.getSkyManager()->setMasserState(mMasser.calculateState(time)); mRendering.getSkyManager()->setSecundaState(mSecunda.calculateState(time)); mRendering.configureFog(mResult.mFogDepth, underwaterFog, mResult.mDLFogFactor, mResult.mDLFogOffset/100.0f, mResult.mFogColor); mRendering.setAmbientColour(mResult.mAmbientColor); mRendering.setSunColour(mResult.mSunColor, mResult.mSunColor * mResult.mGlareView); mRendering.getSkyManager()->setWeather(mResult); // Play sounds if (mPlayingSoundID != mResult.mAmbientLoopSoundID) { stopSounds(); if (!mResult.mAmbientLoopSoundID.empty()) mAmbientSound = MWBase::Environment::get().getSoundManager()->playSound( mResult.mAmbientLoopSoundID, mResult.mAmbientSoundVolume, 1.0, MWSound::Type::Sfx, MWSound::PlayMode::Loop ); mPlayingSoundID = mResult.mAmbientLoopSoundID; } else if (mAmbientSound) mAmbientSound->setVolume(mResult.mAmbientSoundVolume); } void WeatherManager::stopSounds() { if (mAmbientSound) MWBase::Environment::get().getSoundManager()->stopSound(mAmbientSound); mAmbientSound = nullptr; mPlayingSoundID.clear(); } float WeatherManager::getWindSpeed() const { return mWindSpeed; } bool WeatherManager::isInStorm() const { return mIsStorm; } osg::Vec3f WeatherManager::getStormDirection() const { return mStormDirection; } void WeatherManager::advanceTime(double hours, bool incremental) { // In Morrowind, when the player sleeps/waits, serves jail time, travels, or trains, all weather transitions are // immediately applied, regardless of whatever transition time might have been remaining. mTimePassed += hours; mFastForward = !incremental ? true : mFastForward; } unsigned int WeatherManager::getWeatherID() const { return mCurrentWeather; } bool WeatherManager::useTorches(float hour) const { bool isDark = hour < mSunriseTime || hour > mTimeSettings.mNightStart; return isDark && !mPrecipitation; } void WeatherManager::write(ESM::ESMWriter& writer, Loading::Listener& progress) { ESM::WeatherState state; state.mCurrentRegion = mCurrentRegion; state.mTimePassed = mTimePassed; state.mFastForward = mFastForward; state.mWeatherUpdateTime = mWeatherUpdateTime; state.mTransitionFactor = mTransitionFactor; state.mCurrentWeather = mCurrentWeather; state.mNextWeather = mNextWeather; state.mQueuedWeather = mQueuedWeather; std::map::iterator it = mRegions.begin(); for(; it != mRegions.end(); ++it) { state.mRegions.insert(std::make_pair(it->first, it->second)); } writer.startRecord(ESM::REC_WTHR); state.save(writer); writer.endRecord(ESM::REC_WTHR); } bool WeatherManager::readRecord(ESM::ESMReader& reader, uint32_t type) { if(ESM::REC_WTHR == type) { static const int oldestCompatibleSaveFormat = 2; if(reader.getFormat() < oldestCompatibleSaveFormat) { // Weather state isn't really all that important, so to preserve older save games, we'll just discard the // older weather records, rather than fail to handle the record. reader.skipRecord(); } else { ESM::WeatherState state; state.load(reader); mCurrentRegion.swap(state.mCurrentRegion); mTimePassed = state.mTimePassed; mFastForward = state.mFastForward; mWeatherUpdateTime = state.mWeatherUpdateTime; mTransitionFactor = state.mTransitionFactor; mCurrentWeather = state.mCurrentWeather; mNextWeather = state.mNextWeather; mQueuedWeather = state.mQueuedWeather; mRegions.clear(); importRegions(); for(std::map::iterator it = state.mRegions.begin(); it != state.mRegions.end(); ++it) { std::map::iterator found = mRegions.find(it->first); if (found != mRegions.end()) { found->second = RegionWeather(it->second); } } } return true; } return false; } void WeatherManager::clear() { stopSounds(); mCurrentRegion = ""; mTimePassed = 0.0f; mWeatherUpdateTime = 0.0f; forceWeather(0); mRegions.clear(); importRegions(); } /* Start of tes3mp addition Make it possible to check whether the local WeatherManager has the ability to create weather changes */ bool WeatherManager::getWeatherCreationState() { return mWeatherCreationState; } /* End of tes3mp addition */ /* Start of tes3mp addition Make it possible to enable and disable the local WeatherManager's ability to create weather changes */ void WeatherManager::setWeatherCreationState(bool state) { mWeatherCreationState = state; } /* End of tes3mp addition */ /* Start of tes3mp addition Make it possible to send the current weather in a WorldWeather packet when requested from elsewhere in the code */ void WeatherManager::sendWeather() { mwmp::Worldstate *worldstate = mwmp::Main::get().getNetworking()->getWorldstate(); worldstate->sendWeather(mCurrentRegion, mCurrentWeather, mNextWeather, mQueuedWeather, mTransitionFactor); } /* End of tes3mp addition */ inline void WeatherManager::addWeather(const std::string& name, const Fallback::Map& fallback, float dlFactor, float dlOffset, const std::string& particleEffect) { static const float fStromWindSpeed = mStore.get().find("fStromWindSpeed")->getFloat(); Weather weather(name, fallback, fStromWindSpeed, mRainSpeed, dlFactor, dlOffset, particleEffect); mWeatherSettings.push_back(weather); } inline void WeatherManager::importRegions() { Store::iterator it = mStore.get().begin(); for(; it != mStore.get().end(); ++it) { std::string regionID = Misc::StringUtils::lowerCase(it->mId); mRegions.insert(std::make_pair(regionID, RegionWeather(*it))); } } inline void WeatherManager::regionalWeatherChanged(const std::string& regionID, RegionWeather& region) { // If the region is current, then add a weather transition for it. MWWorld::ConstPtr player = MWMechanics::getPlayer(); if(player.isInCell()) { if(Misc::StringUtils::ciEqual(regionID, mCurrentRegion)) { addWeatherTransition(region.getWeather()); } } } inline bool WeatherManager::updateWeatherTime() { /* Start of tes3mp change (major) Avoid creating any weather changes on this client unless approved by the server */ if (!mWeatherCreationState) return false; /* End of tes3mp change (major) */ mWeatherUpdateTime -= mTimePassed; mTimePassed = 0.0f; if(mWeatherUpdateTime <= 0.0f) { // Expire all regional weather, so that any call to getWeather() will return a new weather ID. std::map::iterator it = mRegions.begin(); for(; it != mRegions.end(); ++it) { it->second.setWeather(invalidWeatherID); } mWeatherUpdateTime += mHoursBetweenWeatherChanges; return true; } return false; } inline bool WeatherManager::updateWeatherRegion(const std::string& playerRegion) { if(!playerRegion.empty() && playerRegion != mCurrentRegion) { mCurrentRegion = playerRegion; /* Start of tes3mp addition If we've moved to another region, set our weather creation ability to false; the server will set it to true if it wants us creating weather here */ setWeatherCreationState(false); /* End of tes3mp addition */ return true; } return false; } inline void WeatherManager::updateWeatherTransitions(const float elapsedRealSeconds) { /* Start of tes3mp addition Track whether an ID_WORLD_WEATHER packet should be sent or not */ bool shouldSendPacket = false; /* End of tes3mp addition */ // When a player chooses to train, wait, or serves jail time, any transitions will be fast forwarded to the last // weather type set, regardless of the remaining transition time. if(!mFastForward && inTransition()) { const float delta = mWeatherSettings[mNextWeather].transitionDelta(); mTransitionFactor -= elapsedRealSeconds * delta; if(mTransitionFactor <= 0.0f) { mCurrentWeather = mNextWeather; mNextWeather = mQueuedWeather; mQueuedWeather = invalidWeatherID; // We may have begun processing the queued transition, so we need to apply the remaining time towards it. if(inTransition()) { const float newDelta = mWeatherSettings[mNextWeather].transitionDelta(); const float remainingSeconds = -(mTransitionFactor / delta); mTransitionFactor = 1.0f - (remainingSeconds * newDelta); } else { mTransitionFactor = 0.0f; } /* Start of tes3mp addition The weather is changing, so decide to send an ID_WORLD_WEATHER packet */ shouldSendPacket = true; /* End of tes3mp addition */ } } else { /* Start of tes3mp addition If the weather is changing, decide to send an ID_WORLD_WEATHER packet */ if (mQueuedWeather != invalidWeatherID || mNextWeather != invalidWeatherID) shouldSendPacket = true; /* End of tes3mp addition */ if(mQueuedWeather != invalidWeatherID) { mCurrentWeather = mQueuedWeather; } else if(mNextWeather != invalidWeatherID) { mCurrentWeather = mNextWeather; } mNextWeather = invalidWeatherID; mQueuedWeather = invalidWeatherID; mFastForward = false; } /* Start of tes3mp addition Send an ID_WORLD_WEATHER packet every time the weather changes here, but only if we are allowed to create weather changes on this client */ if (shouldSendPacket && mWeatherCreationState && !mCurrentRegion.empty()) { sendWeather(); } /* End of tes3mp addition */ } inline void WeatherManager::forceWeather(const int weatherID) { mTransitionFactor = 0.0f; mCurrentWeather = weatherID; mNextWeather = invalidWeatherID; mQueuedWeather = invalidWeatherID; /* Start of tes3mp addition Send an ID_WORLD_WEATHER packet every time the weather changes here, but only if we are allowed to create weather changes on this client */ if (!mCurrentRegion.empty() && mWeatherCreationState) { sendWeather(); } /* End of tes3mp addition */ } inline bool WeatherManager::inTransition() { return mNextWeather != invalidWeatherID; } inline void WeatherManager::addWeatherTransition(const int weatherID) { // In order to work like ChangeWeather expects, this method begins transitioning to the new weather immediately if // no transition is in progress, otherwise it queues it to be transitioned. assert(weatherID >= 0 && static_cast(weatherID) < mWeatherSettings.size()); if(!inTransition() && (weatherID != mCurrentWeather)) { mNextWeather = weatherID; mTransitionFactor = 1.0f; } else if(inTransition() && (weatherID != mNextWeather)) { mQueuedWeather = weatherID; } /* Start of tes3mp addition Send an ID_WORLD_WEATHER packet every time the weather changes here, but only if we are allowed to create weather changes on this client */ if (mWeatherCreationState) { sendWeather(); } /* End of tes3mp addition */ } inline void WeatherManager::calculateWeatherResult(const float gameHour, const float elapsedSeconds, const bool isPaused) { float flash = 0.0f; if(!inTransition()) { calculateResult(mCurrentWeather, gameHour); flash = mWeatherSettings[mCurrentWeather].calculateThunder(1.0f, elapsedSeconds, isPaused); } else { calculateTransitionResult(1 - mTransitionFactor, gameHour); float currentFlash = mWeatherSettings[mCurrentWeather].calculateThunder(mTransitionFactor, elapsedSeconds, isPaused); float nextFlash = mWeatherSettings[mNextWeather].calculateThunder(1 - mTransitionFactor, elapsedSeconds, isPaused); flash = currentFlash + nextFlash; } osg::Vec4f flashColor(flash, flash, flash, 0.0f); mResult.mFogColor += flashColor; mResult.mAmbientColor += flashColor; mResult.mSunColor += flashColor; } inline void WeatherManager::calculateResult(const int weatherID, const float gameHour) { const Weather& current = mWeatherSettings[weatherID]; mResult.mCloudTexture = current.mCloudTexture; mResult.mCloudBlendFactor = 0; mResult.mWindSpeed = current.mWindSpeed; mResult.mCloudSpeed = current.mCloudSpeed; mResult.mGlareView = current.mGlareView; mResult.mAmbientLoopSoundID = current.mAmbientLoopSoundID; mResult.mAmbientSoundVolume = 1.f; mResult.mEffectFade = 1.f; mResult.mIsStorm = current.mIsStorm; mResult.mRainSpeed = current.mRainSpeed; mResult.mRainFrequency = current.mRainFrequency; mResult.mParticleEffect = current.mParticleEffect; mResult.mRainEffect = current.mRainEffect; mResult.mNight = (gameHour < mSunriseTime || gameHour > mTimeSettings.mNightStart + mTimeSettings.mStarsPostSunsetStart - mTimeSettings.mStarsFadingDuration); mResult.mFogDepth = current.mLandFogDepth.getValue(gameHour, mTimeSettings, "Fog"); mResult.mFogColor = current.mFogColor.getValue(gameHour, mTimeSettings, "Fog"); mResult.mAmbientColor = current.mAmbientColor.getValue(gameHour, mTimeSettings, "Ambient"); mResult.mSunColor = current.mSunColor.getValue(gameHour, mTimeSettings, "Sun"); mResult.mSkyColor = current.mSkyColor.getValue(gameHour, mTimeSettings, "Sky"); mResult.mNightFade = mNightFade.getValue(gameHour, mTimeSettings, "Stars"); mResult.mDLFogFactor = current.mDL.FogFactor; mResult.mDLFogOffset = current.mDL.FogOffset; WeatherSetting setting = mTimeSettings.getSetting("Sun"); float preSunsetTime = setting.mPreSunsetTime; if (gameHour >= mTimeSettings.mDayEnd - preSunsetTime) { float factor = 1.f; if (preSunsetTime > 0) factor = (gameHour - (mTimeSettings.mDayEnd - preSunsetTime)) / preSunsetTime; factor = std::min(1.f, factor); mResult.mSunDiscColor = lerp(osg::Vec4f(1,1,1,1), current.mSunDiscSunsetColor, factor); // The SunDiscSunsetColor in the INI isn't exactly the resulting color on screen, most likely because // MW applied the color to the ambient term as well. After the ambient and emissive terms are added together, the fixed pipeline // would then clamp the total lighting to (1,1,1). A noticeable change in color tone can be observed when only one of the color components gets clamped. // Unfortunately that means we can't use the INI color as is, have to replicate the above nonsense. mResult.mSunDiscColor = mResult.mSunDiscColor + osg::componentMultiply(mResult.mSunDiscColor, mResult.mAmbientColor); for (int i=0; i<3; ++i) mResult.mSunDiscColor[i] = std::min(1.f, mResult.mSunDiscColor[i]); } else mResult.mSunDiscColor = osg::Vec4f(1,1,1,1); if (gameHour >= mTimeSettings.mDayEnd) { // sunset float fade = std::min(1.f, (gameHour - mTimeSettings.mDayEnd) / (mTimeSettings.mNightStart - mTimeSettings.mDayEnd)); fade = fade*fade; mResult.mSunDiscColor.a() = 1.f - fade; } else if (gameHour >= mTimeSettings.mNightEnd && gameHour <= mTimeSettings.mNightEnd + mSunriseDuration / 2.f) { // sunrise mResult.mSunDiscColor.a() = gameHour - mTimeSettings.mNightEnd; } else mResult.mSunDiscColor.a() = 1; } inline void WeatherManager::calculateTransitionResult(const float factor, const float gameHour) { calculateResult(mCurrentWeather, gameHour); const MWRender::WeatherResult current = mResult; calculateResult(mNextWeather, gameHour); const MWRender::WeatherResult other = mResult; mResult.mCloudTexture = current.mCloudTexture; mResult.mNextCloudTexture = other.mCloudTexture; mResult.mCloudBlendFactor = mWeatherSettings[mNextWeather].cloudBlendFactor(factor); mResult.mFogColor = lerp(current.mFogColor, other.mFogColor, factor); mResult.mSunColor = lerp(current.mSunColor, other.mSunColor, factor); mResult.mSkyColor = lerp(current.mSkyColor, other.mSkyColor, factor); mResult.mAmbientColor = lerp(current.mAmbientColor, other.mAmbientColor, factor); mResult.mSunDiscColor = lerp(current.mSunDiscColor, other.mSunDiscColor, factor); mResult.mFogDepth = lerp(current.mFogDepth, other.mFogDepth, factor); mResult.mDLFogFactor = lerp(current.mDLFogFactor, other.mDLFogFactor, factor); mResult.mDLFogOffset = lerp(current.mDLFogOffset, other.mDLFogOffset, factor); mResult.mWindSpeed = lerp(current.mWindSpeed, other.mWindSpeed, factor); mResult.mCloudSpeed = lerp(current.mCloudSpeed, other.mCloudSpeed, factor); mResult.mGlareView = lerp(current.mGlareView, other.mGlareView, factor); mResult.mNightFade = lerp(current.mNightFade, other.mNightFade, factor); mResult.mNight = current.mNight; if(factor < 0.5) { mResult.mIsStorm = current.mIsStorm; mResult.mParticleEffect = current.mParticleEffect; mResult.mRainEffect = current.mRainEffect; mResult.mRainSpeed = current.mRainSpeed; mResult.mRainFrequency = current.mRainFrequency; mResult.mAmbientSoundVolume = 1-(factor*2); mResult.mEffectFade = mResult.mAmbientSoundVolume; mResult.mAmbientLoopSoundID = current.mAmbientLoopSoundID; } else { mResult.mIsStorm = other.mIsStorm; mResult.mParticleEffect = other.mParticleEffect; mResult.mRainEffect = other.mRainEffect; mResult.mRainSpeed = other.mRainSpeed; mResult.mRainFrequency = other.mRainFrequency; mResult.mAmbientSoundVolume = 2*(factor-0.5f); mResult.mEffectFade = mResult.mAmbientSoundVolume; mResult.mAmbientLoopSoundID = other.mAmbientLoopSoundID; } }