123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652 |
- /*
- * 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 Nini.Config;
- using OpenMetaverse;
- using OpenSim.Framework;
- using OpenSim.Region.Framework.Interfaces;
- using OpenSim.Region.Framework.Scenes;
- namespace OpenSim.Region.CoreModules
- {
- public class SunModule : ISunModule
- {
- /// <summary>
- /// 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.
- /// </summary>
- 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;
- /// <summary>
- /// Calculate the sun's orbital position and its velocity.
- /// </summary>
- 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 infor with new Sun Position and Hour
- // set estate settings for region access to sun position
- if (receivedEstateToolsSunUpdate)
- {
- m_scene.RegionInfo.RegionSettings.SunVector = Position;
- m_scene.RegionInfo.RegionSettings.SunPosition = GetCurrentTimeAsLindenSunHour();
- }
- }
- private float GetCurrentTimeAsLindenSunHour()
- {
- if (m_SunFixed)
- return m_SunFixedHour + 6;
- return GetCurrentSunHour() + 6.0f;
- }
- #region IRegion Methods
- // Called immediately after the module is loaded for a given region
- // i.e. Immediately after instance creation.
- public void Initialise(Scene scene, IConfigSource config)
- {
- m_scene = scene;
- m_frame = 0;
- // This one puts an entry in the main help screen
- m_scene.AddCommand(this, String.Empty, "sun", "Usage: sun [param] [value] - Get or Update Sun module paramater", null);
- // This one enables the ability to type just "sun" without any parameters
- m_scene.AddCommand(this, "sun", "", "", HandleSunConsoleCommand);
- foreach (KeyValuePair<string, string> kvp in GetParamList())
- {
- m_scene.AddCommand(this, String.Format("sun {0}", kvp.Key), String.Format("{0} - {1}", kvp.Key, kvp.Value), "", HandleSunConsoleCommand);
- }
- TimeZone local = TimeZone.CurrentTimeZone;
- TicksUTCOffset = local.GetUtcOffset(local.ToLocalTime(DateTime.Now)).Ticks;
- m_log.Debug("[SUN]: localtime offset is " + 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;
- // Insert our event handling hooks
- scene.EventManager.OnFrame += SunUpdate;
- scene.EventManager.OnAvatarEnteringNewParcel += AvatarEnteringParcel;
- scene.EventManager.OnEstateToolsSunUpdate += EstateToolsSunUpdate;
- scene.EventManager.OnGetCurrentTimeAsLindenSunHour += GetCurrentTimeAsLindenSunHour;
- ready = true;
- 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;
- }
- scene.RegisterModuleInterface<ISunModule>(this);
- }
- public void PostInitialise()
- {
- }
- public void Close()
- {
- 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 string Name
- {
- get { return "SunModule"; }
- }
- public bool IsSharedModule
- {
- get { return false; }
- }
- #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();
- }
- /// <summary>
- /// When an avatar enters the region, it's probably a good idea to send them the current sun info
- /// </summary>
- /// <param name="avatar"></param>
- /// <param name="localLandID"></param>
- /// <param name="regionID"></param>
- private void AvatarEnteringParcel(ScenePresence avatar, int localLandID, UUID regionID)
- {
- SunToClient(avatar.ControllingClient);
- }
- /// <summary>
- ///
- /// </summary>
- /// <param name="regionHandle"></param>
- /// <param name="FixedTime">Is the sun's position fixed?</param>
- /// <param name="useEstateTime">Use the Region or Estate Sun hour?</param>
- /// <param name="FixedSunHour">What hour of the day is the Sun Fixed at?</param>
- public void EstateToolsSunUpdate(ulong regionHandle, bool FixedSun, bool useEstateTime, float FixedSunHour)
- {
- if (m_scene.RegionInfo.RegionHandle == regionHandle)
- {
- // Must limit the Sun Hour to 0 ... 24
- while (FixedSunHour > 24.0f)
- FixedSunHour -= 24;
- while (FixedSunHour < 0)
- FixedSunHour += 24;
- m_SunFixedHour = FixedSunHour;
- 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.ForEachScenePresence(delegate(ScenePresence sp)
- {
- if (!sp.IsChildAgent)
- {
- SunToClient(sp.ControllingClient);
- }
- });
- }
- #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<string, string> GetParamList()
- {
- Dictionary<string, string> Params = new Dictionary<string, string>();
- 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<string> ParseCmdParams(string[] args)
- {
- List<string> Output = new List<string>();
- if ((args.Length == 1) || (args[1].ToLower() == "help") || (args[1].ToLower() == "list"))
- {
- Output.Add("The following parameters can be changed or viewed:");
- foreach (KeyValuePair<string, string> 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;
- }
- }
- }
|