/*
* Copyright (c) Contributors, http://opensimulator.org/
* See CONTRIBUTORS.TXT for a full list of copyright holders.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of the OpenSimulator Project nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
using System;
using System.Collections.Generic;
using System.Reflection;
using log4net;
using Mono.Addins;
using Nini.Config;
using OpenMetaverse;
using OpenSim.Framework;
using OpenSim.Region.Framework.Interfaces;
using OpenSim.Region.Framework.Scenes;
namespace OpenSim.Region.CoreModules
{
[Extension(Path = "/OpenSim/RegionModules", NodeName = "RegionModule", Id = "SunModule")]
public class SunModule : ISunModule
{
///
/// Note: Sun Hour can be a little deceaving. Although it's based on a 24 hour clock
/// it is not based on ~06:00 == Sun Rise. Rather it is based on 00:00 being sun-rise.
///
private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
//
// Global Constants used to determine where in the sky the sun is
//
private const double m_SeasonalTilt = 0.03 * Math.PI; // A daily shift of approximately 1.7188 degrees
private const double m_AverageTilt = -0.25 * Math.PI; // A 45 degree tilt
private const double m_SunCycle = 2.0D * Math.PI; // A perfect circle measured in radians
private const double m_SeasonalCycle = 2.0D * Math.PI; // Ditto
//
// Per Region Values
//
private bool ready = false;
// This solves a chick before the egg problem
// the local SunFixedHour and SunFixed variables MUST be updated
// at least once with the proper Region Settings before we start
// updating those region settings in GenSunPos()
private bool receivedEstateToolsSunUpdate = false;
// Configurable values
private string m_RegionMode = "SL";
// Sun's position information is updated and sent to clients every m_UpdateInterval frames
private int m_UpdateInterval = 0;
// Number of real time hours per virtual day
private double m_DayLengthHours = 0;
// Number of virtual days to a virtual year
private int m_YearLengthDays = 0;
// Ratio of Daylight hours to Night time hours. This is accomplished by shifting the
// sun's orbit above the horizon
private double m_HorizonShift = 0;
// Used to scale current and positional time to adjust length of an hour during day vs night.
private double m_DayTimeSunHourScale;
// private double m_longitude = 0;
// private double m_latitude = 0;
// Configurable defaults Defaults close to SL
private string d_mode = "SL";
private int d_frame_mod = 100; // Every 10 seconds (actually less)
private double d_day_length = 4; // A VW day is 4 RW hours long
private int d_year_length = 60; // There are 60 VW days in a VW year
private double d_day_night = 0.5; // axis offset: Default Hoizon shift to try and closely match the sun model in LL Viewer
private double d_DayTimeSunHourScale = 0.5; // Day/Night hours are equal
// private double d_longitude = -73.53;
// private double d_latitude = 41.29;
// Frame counter
private uint m_frame = 0;
// Cached Scene reference
private Scene m_scene = null;
// Calculated Once in the lifetime of a region
private long TicksToEpoch; // Elapsed time for 1/1/1970
private uint SecondsPerSunCycle; // Length of a virtual day in RW seconds
private uint SecondsPerYear; // Length of a virtual year in RW seconds
private double SunSpeed; // Rate of passage in radians/second
private double SeasonSpeed; // Rate of change for seasonal effects
// private double HoursToRadians; // Rate of change for seasonal effects
private long TicksUTCOffset = 0; // seconds offset from UTC
// Calculated every update
private float OrbitalPosition; // Orbital placement at a point in time
private double HorizonShift; // Axis offset to skew day and night
private double TotalDistanceTravelled; // Distance since beginning of time (in radians)
private double SeasonalOffset; // Seaonal variation of tilt
private float Magnitude; // Normal tilt
// private double VWTimeRatio; // VW time as a ratio of real time
// Working values
private Vector3 Position = Vector3.Zero;
private Vector3 Velocity = Vector3.Zero;
private Quaternion Tilt = new Quaternion(1.0f, 0.0f, 0.0f, 0.0f);
// Used to fix the sun in the sky so it doesn't move based on current time
private bool m_SunFixed = false;
private float m_SunFixedHour = 0f;
private const int TICKS_PER_SECOND = 10000000;
// Current time in elapsed seconds since Jan 1st 1970
private ulong CurrentTime
{
get
{
return (ulong)(((DateTime.Now.Ticks) - TicksToEpoch + TicksUTCOffset) / TICKS_PER_SECOND);
}
}
// Time in seconds since UTC to use to calculate sun position.
ulong PosTime = 0;
///
/// Calculate the sun's orbital position and its velocity.
///
private void GenSunPos()
{
// Time in seconds since UTC to use to calculate sun position.
PosTime = CurrentTime;
if (m_SunFixed)
{
// SunFixedHour represents the "hour of day" we would like
// It's represented in 24hr time, with 0 hour being sun-rise
// Because our day length is probably not 24hrs {LL is 6} we need to do a bit of math
// Determine the current "day" from current time, so we can use "today"
// to determine Seasonal Tilt and what'not
// Integer math rounded is on purpose to drop fractional day, determines number
// of virtual days since Epoch
PosTime = CurrentTime / SecondsPerSunCycle;
// Since we want number of seconds since Epoch, multiply back up
PosTime *= SecondsPerSunCycle;
// Then offset by the current Fixed Sun Hour
// Fixed Sun Hour needs to be scaled to reflect the user configured Seconds Per Sun Cycle
PosTime += (ulong)((m_SunFixedHour / 24.0) * (ulong)SecondsPerSunCycle);
}
else
{
if (m_DayTimeSunHourScale != 0.5f)
{
ulong CurDaySeconds = CurrentTime % SecondsPerSunCycle;
double CurDayPercentage = (double)CurDaySeconds / SecondsPerSunCycle;
ulong DayLightSeconds = (ulong)(m_DayTimeSunHourScale * SecondsPerSunCycle);
ulong NightSeconds = SecondsPerSunCycle - DayLightSeconds;
PosTime = CurrentTime / SecondsPerSunCycle;
PosTime *= SecondsPerSunCycle;
if (CurDayPercentage < 0.5)
{
PosTime += (ulong)((CurDayPercentage / .5) * DayLightSeconds);
}
else
{
PosTime += DayLightSeconds;
PosTime += (ulong)(((CurDayPercentage - 0.5) / .5) * NightSeconds);
}
}
}
TotalDistanceTravelled = SunSpeed * PosTime; // distance measured in radians
OrbitalPosition = (float)(TotalDistanceTravelled % m_SunCycle); // position measured in radians
// TotalDistanceTravelled += HoursToRadians-(0.25*Math.PI)*Math.Cos(HoursToRadians)-OrbitalPosition;
// OrbitalPosition = (float) (TotalDistanceTravelled%SunCycle);
SeasonalOffset = SeasonSpeed * PosTime; // Present season determined as total radians travelled around season cycle
Tilt.W = (float)(m_AverageTilt + (m_SeasonalTilt * Math.Sin(SeasonalOffset))); // Calculate seasonal orbital N/S tilt
// m_log.Debug("[SUN] Total distance travelled = "+TotalDistanceTravelled+", present position = "+OrbitalPosition+".");
// m_log.Debug("[SUN] Total seasonal progress = "+SeasonalOffset+", present tilt = "+Tilt.W+".");
// The sun rotates about the Z axis
Position.X = (float)Math.Cos(-TotalDistanceTravelled);
Position.Y = (float)Math.Sin(-TotalDistanceTravelled);
Position.Z = 0;
// For interest we rotate it slightly about the X access.
// Celestial tilt is a value that ranges .025
Position *= Tilt;
// Finally we shift the axis so that more of the
// circle is above the horizon than below. This
// makes the nights shorter than the days.
Position = Vector3.Normalize(Position);
Position.Z = Position.Z + (float)HorizonShift;
Position = Vector3.Normalize(Position);
// m_log.Debug("[SUN] Position("+Position.X+","+Position.Y+","+Position.Z+")");
Velocity.X = 0;
Velocity.Y = 0;
Velocity.Z = (float)SunSpeed;
// Correct angular velocity to reflect the seasonal rotation
Magnitude = Position.Length();
if (m_SunFixed)
{
Velocity.X = 0;
Velocity.Y = 0;
Velocity.Z = 0;
}
else
{
Velocity = (Velocity * Tilt) * (1.0f / Magnitude);
}
// TODO: Decouple this, so we can get rid of Linden Hour info
// Update Region with new Sun Vector
// set estate settings for region access to sun position
if (receivedEstateToolsSunUpdate)
{
m_scene.RegionInfo.RegionSettings.SunVector = Position;
}
}
private float GetCurrentTimeAsLindenSunHour()
{
if (m_SunFixed)
return m_SunFixedHour + 6;
return GetCurrentSunHour() + 6.0f;
}
#region INonSharedRegion Methods
// Called immediately after the module is loaded for a given region
// i.e. Immediately after instance creation.
public void Initialise(IConfigSource config)
{
m_frame = 0;
// This one puts an entry in the main help screen
// m_scene.AddCommand("Regions", this, "sun", "sun", "Usage: sun [param] [value] - Get or Update Sun module paramater", null);
TimeZone local = TimeZone.CurrentTimeZone;
TicksUTCOffset = local.GetUtcOffset(local.ToLocalTime(DateTime.Now)).Ticks;
m_log.DebugFormat("[SUN]: localtime offset is {0}", TicksUTCOffset);
// Align ticks with Second Life
TicksToEpoch = new DateTime(1970, 1, 1).Ticks;
// Just in case they don't have the stanzas
try
{
// Mode: determines how the sun is handled
m_RegionMode = config.Configs["Sun"].GetString("mode", d_mode);
// Mode: determines how the sun is handled
// m_latitude = config.Configs["Sun"].GetDouble("latitude", d_latitude);
// Mode: determines how the sun is handled
// m_longitude = config.Configs["Sun"].GetDouble("longitude", d_longitude);
// Year length in days
m_YearLengthDays = config.Configs["Sun"].GetInt("year_length", d_year_length);
// Day length in decimal hours
m_DayLengthHours = config.Configs["Sun"].GetDouble("day_length", d_day_length);
// Horizon shift, this is used to shift the sun's orbit, this affects the day / night ratio
// must hard code to ~.5 to match sun position in LL based viewers
m_HorizonShift = config.Configs["Sun"].GetDouble("day_night_offset", d_day_night);
// Scales the sun hours 0...12 vs 12...24, essentially makes daylight hours longer/shorter vs nighttime hours
m_DayTimeSunHourScale = config.Configs["Sun"].GetDouble("day_time_sun_hour_scale", d_DayTimeSunHourScale);
// Update frequency in frames
m_UpdateInterval = config.Configs["Sun"].GetInt("update_interval", d_frame_mod);
}
catch (Exception e)
{
m_log.Debug("[SUN]: Configuration access failed, using defaults. Reason: " + e.Message);
m_RegionMode = d_mode;
m_YearLengthDays = d_year_length;
m_DayLengthHours = d_day_length;
m_HorizonShift = d_day_night;
m_UpdateInterval = d_frame_mod;
m_DayTimeSunHourScale = d_DayTimeSunHourScale;
// m_latitude = d_latitude;
// m_longitude = d_longitude;
}
switch (m_RegionMode)
{
case "T1":
default:
case "SL":
// Time taken to complete a cycle (day and season)
SecondsPerSunCycle = (uint) (m_DayLengthHours * 60 * 60);
SecondsPerYear = (uint) (SecondsPerSunCycle*m_YearLengthDays);
// Ration of real-to-virtual time
// VWTimeRatio = 24/m_day_length;
// Speed of rotation needed to complete a cycle in the
// designated period (day and season)
SunSpeed = m_SunCycle/SecondsPerSunCycle;
SeasonSpeed = m_SeasonalCycle/SecondsPerYear;
// Horizon translation
HorizonShift = m_HorizonShift; // Z axis translation
// HoursToRadians = (SunCycle/24)*VWTimeRatio;
m_log.Debug("[SUN]: Mode is " + m_RegionMode);
m_log.Debug("[SUN]: Initialization completed. Day is " + SecondsPerSunCycle + " seconds, and year is " + m_YearLengthDays + " days");
m_log.Debug("[SUN]: Axis offset is " + m_HorizonShift);
m_log.Debug("[SUN]: Percentage of time for daylight " + m_DayTimeSunHourScale);
m_log.Debug("[SUN]: Positional data updated every " + m_UpdateInterval + " frames");
break;
}
}
public Type ReplaceableInterface
{
get { return null; }
}
public void AddRegion(Scene scene)
{
m_scene = scene;
// Insert our event handling hooks
scene.EventManager.OnFrame += SunUpdate;
scene.EventManager.OnAvatarEnteringNewParcel += AvatarEnteringParcel;
scene.EventManager.OnEstateToolsSunUpdate += EstateToolsSunUpdate;
scene.EventManager.OnGetCurrentTimeAsLindenSunHour += GetCurrentTimeAsLindenSunHour;
scene.RegisterModuleInterface(this);
// This one enables the ability to type just "sun" without any parameters
// m_scene.AddCommand("Regions", this, "sun", "", "", HandleSunConsoleCommand);
foreach (KeyValuePair kvp in GetParamList())
{
string sunCommand = string.Format("sun {0}", kvp.Key);
m_scene.AddCommand("Regions", this, sunCommand, string.Format("{0} []", sunCommand), kvp.Value, "", HandleSunConsoleCommand);
}
ready = true;
}
public void RemoveRegion(Scene scene)
{
ready = false;
// Remove our hooks
m_scene.EventManager.OnFrame -= SunUpdate;
m_scene.EventManager.OnAvatarEnteringNewParcel -= AvatarEnteringParcel;
m_scene.EventManager.OnEstateToolsSunUpdate -= EstateToolsSunUpdate;
m_scene.EventManager.OnGetCurrentTimeAsLindenSunHour -= GetCurrentTimeAsLindenSunHour;
}
public void RegionLoaded(Scene scene)
{
}
public void Close()
{
}
public string Name
{
get { return "SunModule"; }
}
#endregion
#region EventManager Events
public void SunToClient(IClientAPI client)
{
if (m_RegionMode != "T1")
{
if (ready)
{
if (m_SunFixed)
{
// m_log.DebugFormat("[SUN]: SunHour {0}, Position {1}, PosTime {2}, OrbitalPosition : {3} ", m_SunFixedHour, Position.ToString(), PosTime.ToString(), OrbitalPosition.ToString());
client.SendSunPos(Position, Velocity, PosTime, SecondsPerSunCycle, SecondsPerYear, OrbitalPosition);
}
else
{
// m_log.DebugFormat("[SUN]: SunHour {0}, Position {1}, PosTime {2}, OrbitalPosition : {3} ", m_SunFixedHour, Position.ToString(), PosTime.ToString(), OrbitalPosition.ToString());
client.SendSunPos(Position, Velocity, CurrentTime, SecondsPerSunCycle, SecondsPerYear, OrbitalPosition);
}
}
}
}
public void SunUpdate()
{
if (((m_frame++ % m_UpdateInterval) != 0) || !ready || m_SunFixed || !receivedEstateToolsSunUpdate)
return;
GenSunPos(); // Generate shared values once
SunUpdateToAllClients();
}
///
/// When an avatar enters the region, it's probably a good idea to send them the current sun info
///
///
///
///
private void AvatarEnteringParcel(ScenePresence avatar, int localLandID, UUID regionID)
{
SunToClient(avatar.ControllingClient);
}
public void EstateToolsSunUpdate(ulong regionHandle)
{
if (m_scene.RegionInfo.RegionHandle == regionHandle)
{
float sunFixedHour;
bool fixedSun;
if (m_scene.RegionInfo.RegionSettings.UseEstateSun)
{
sunFixedHour = (float)m_scene.RegionInfo.EstateSettings.SunPosition;
fixedSun = m_scene.RegionInfo.EstateSettings.FixedSun;
}
else
{
sunFixedHour = (float)m_scene.RegionInfo.RegionSettings.SunPosition - 6.0f;
fixedSun = m_scene.RegionInfo.RegionSettings.FixedSun;
}
// Must limit the Sun Hour to 0 ... 24
while (sunFixedHour > 24.0f)
sunFixedHour -= 24;
while (sunFixedHour < 0)
sunFixedHour += 24;
m_SunFixedHour = sunFixedHour;
m_SunFixed = fixedSun;
// m_log.DebugFormat("[SUN]: Sun Settings Update: Fixed Sun? : {0}", m_SunFixed.ToString());
// m_log.DebugFormat("[SUN]: Sun Settings Update: Sun Hour : {0}", m_SunFixedHour.ToString());
receivedEstateToolsSunUpdate = true;
// Generate shared values
GenSunPos();
// When sun settings are updated, we should update all clients with new settings.
SunUpdateToAllClients();
// m_log.DebugFormat("[SUN]: PosTime : {0}", PosTime.ToString());
}
}
#endregion
private void SunUpdateToAllClients()
{
m_scene.ForEachRootClient(delegate(IClientAPI client)
{
SunToClient(client);
});
}
#region ISunModule Members
public double GetSunParameter(string param)
{
switch (param.ToLower())
{
case "year_length":
return m_YearLengthDays;
case "day_length":
return m_DayLengthHours;
case "day_night_offset":
return m_HorizonShift;
case "day_time_sun_hour_scale":
return m_DayTimeSunHourScale;
case "update_interval":
return m_UpdateInterval;
default:
throw new Exception("Unknown sun parameter.");
}
}
public void SetSunParameter(string param, double value)
{
HandleSunConsoleCommand("sun", new string[] {param, value.ToString() });
}
public float GetCurrentSunHour()
{
float ticksleftover = CurrentTime % SecondsPerSunCycle;
return (24.0f * (ticksleftover / SecondsPerSunCycle));
}
#endregion
public void HandleSunConsoleCommand(string module, string[] cmdparams)
{
if (m_scene.ConsoleScene() == null)
{
// FIXME: If console region is root then this will be printed by every module. Currently, there is no
// way to prevent this, short of making the entire module shared (which is complete overkill).
// One possibility is to return a bool to signal whether the module has completely handled the command
m_log.InfoFormat("[Sun]: Please change to a specific region in order to set Sun parameters.");
return;
}
if (m_scene.ConsoleScene() != m_scene)
{
m_log.InfoFormat("[Sun]: Console Scene is not my scene.");
return;
}
m_log.InfoFormat("[Sun]: Processing command.");
foreach (string output in ParseCmdParams(cmdparams))
{
m_log.Info("[SUN] " + output);
}
}
private Dictionary GetParamList()
{
Dictionary Params = new Dictionary();
Params.Add("year_length", "number of days to a year");
Params.Add("day_length", "number of seconds to a day");
Params.Add("day_night_offset", "induces a horizon shift");
Params.Add("update_interval", "how often to update the sun's position in frames");
Params.Add("day_time_sun_hour_scale", "scales day light vs nite hours to change day/night ratio");
return Params;
}
private List ParseCmdParams(string[] args)
{
List Output = new List();
if ((args.Length == 1) || (args[1].ToLower() == "help") || (args[1].ToLower() == "list"))
{
Output.Add("The following parameters can be changed or viewed:");
foreach (KeyValuePair kvp in GetParamList())
{
Output.Add(String.Format("{0} - {1}",kvp.Key, kvp.Value));
}
return Output;
}
if (args.Length == 2)
{
try
{
double value = GetSunParameter(args[1]);
Output.Add(String.Format("Parameter {0} is {1}.", args[1], value.ToString()));
}
catch (Exception)
{
Output.Add(String.Format("Unknown parameter {0}.", args[1]));
}
}
else if (args.Length == 3)
{
float value = 0.0f;
if (!float.TryParse(args[2], out value))
{
Output.Add(String.Format("The parameter value {0} is not a valid number.", args[2]));
}
switch (args[1].ToLower())
{
case "year_length":
m_YearLengthDays = (int)value;
break;
case "day_length":
m_DayLengthHours = value;
break;
case "day_night_offset":
m_HorizonShift = value;
break;
case "day_time_sun_hour_scale":
m_DayTimeSunHourScale = value;
break;
case "update_interval":
m_UpdateInterval = (int)value;
break;
default:
Output.Add(String.Format("Unknown parameter {0}.", args[1]));
return Output;
}
Output.Add(String.Format("Parameter {0} set to {1}.", args[1], value.ToString()));
// Generate shared values
GenSunPos();
// When sun settings are updated, we should update all clients with new settings.
SunUpdateToAllClients();
}
return Output;
}
}
}