Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- using UnityEngine;
- using UnityEngine.SceneManagement;
- using UnityEngine.EventSystems;
- using System;
- using System.Collections;
- using System.Collections.Generic;
- using System.Text;
- using UnityEngine.UI;
- /// <summary>A basic Command Line emulation for Unity3D v5.5+. Just use 'CommandLine.Log()'</summary>
- /// <description>MIT License -- TL;DR -- code is free, don't bother me about it!</description>
- public class CommandLine : MonoBehaviour {
- /// <summary>example of how to populate the command-line with commands</summary>
- public void PopulateWithBasicCommands() {
- //When adding commands, you must add a call below to registerCommand() with its name, implementation method, and help text.
- AddCommand("help", (args)=>{ Log(" - - - -\n"+CommandHelpString()+"\n - - - -"); },
- "prints this help.");
- AddCommand("reload", (args) => { SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex); },
- "reloads current scene.");
- AddCommand("pref", (args) =>{ for(int i=1;i<args.Length;++i){ Log(args[i]+":"+PlayerPrefs.GetString(args[i])); } },
- "shows player prefs value. use: pref [variableName, ...]");
- AddCommand("savepref", (args) =>{
- if(args.Length > 1) {
- PlayerPrefs.SetString(args[1], (args.Length>2)?args[2]:null);
- PlayerPrefs.Save();
- } else { Log("missing arguments"); }
- },
- "save player prefs value. use: pref variableName variableValue]");
- AddCommand("resetprefs", (args) =>{ PlayerPrefs.DeleteAll(); PlayerPrefs.Save(); },
- "clears player prefs.");
- }
- /// <param name="command">name of the command to add (case insensitive)</param>
- /// <param name="handler">code to execute with this command, think standard main</param>
- /// <param name="help">reference information, think concise man-pages</param>
- public void addCommand(string command, CommandHandler handler, string help) {
- commands.Add(command.ToLower(), new Command(command, handler, help));
- }
- public static void AddCommand(string command, CommandHandler handler, string help) {
- Instance.addCommand (command, handler, help);
- }
- /// <param name="commands">dictionary of commands to begin using, replacing old commands</param>
- public void SetCommands(Dictionary<string,Command> commands) { this.commands = commands; }
- /// <summary>replace current commands with no commands</summary>
- public void ClearCommands() { commands.Clear (); }
- /// <summary>command-line handler. think "standard main" from Java or C/C++.
- /// args[0] is the command, args[1] and beyond are the arguments.</summary>
- public delegate void CommandHandler(string[] args);
- public class Command {
- public string command { get; private set; }
- public CommandHandler handler { get; private set; }
- public string help { get; private set; }
- public Command(string command, CommandHandler handler, string helpReferenceText) {
- this.command = command; this.handler = handler; this.help = helpReferenceText;
- }
- }
- /// <summary>watching for commands *about to execute*.</summary>
- public event CommandHandler onCommand;
- /// <summary>known commands</summary>
- private Dictionary<string, Command> commands = new Dictionary<string, Command>();
- /// <summary>console data that should not be modifiable as user input</summary>
- private string nonUserInput = "";
- /// <summary>the most recent user-input submitted</summary>
- private string recentUserInput;
- public string GetMostRecentInput() { return recentUserInput; }
- public static string MostRecentInput() { return Instance.GetMostRecentInput(); }
- /// <returns>a list of usable commands</returns>
- public string CommandHelpString() {
- StringBuilder sb = new StringBuilder ();
- foreach(Command cmd in commands.Values) { sb.Append(((sb.Length>0)?"\n":"")+cmd.command+": "+cmd.help); }
- return sb.ToString();
- }
- /// <summary>resets text to what it should be.</summary>
- /// <returns>presumably the character entered by the user to trigger the refresh</returns>
- public char RefreshText() {
- char typed = '\0';
- if (!sudoAdjustment) {
- sudoAdjustment = true;
- string properOutput = nonUserInput;
- string userInput = GetUserInput ();
- if (userInput == "\n") { userInput = ""; }
- if (inputField.caretPosition > 0 && inputField.caretPosition < nonUserInput.Length
- && inputField.text.Length > nonUserInput.Length) {
- typed = inputField.text [inputField.caretPosition - 1];
- }
- string refreshedText = properOutput + userInput + ((typed!='\0')?typed.ToString():"");
- SetText (refreshedText);
- nonUserInput = properOutput;
- sudoAdjustment = false;
- }
- return typed;
- }
- /// <param name="text">What the the output text should be (turns current user input into text output)</param>
- public void SetText(string text) {
- int cutIndex = CutoffIndexToEnsureLineCount (text, maxLines);
- if (cutIndex != 0) { text = text.Substring (cutIndex); }
- nonUserInput = text;
- if (inputField) { inputField.text = nonUserInput; }
- }
- public int CutoffIndexToEnsureLineCount(String s, int maxLines) {
- int lineCount = 0, columnCount = 0, index;
- for (index = s.Length; index > 0; --index) {
- if (s [index - 1] == '\n' || columnCount++ >= maxColumnsPerLine) {
- lineCount++;
- columnCount = 0;
- if (lineCount >= maxLines) { break; }
- }
- }
- return index;
- }
- /// <summary>turns current user input into text output</summary>
- private void UseCurrentText() { nonUserInput = inputField.text; }
- /// <returns>The output text (not including user input).</returns>
- public string GetText() { return nonUserInput; }
- /// <returns>The all text, including user input</returns>
- public string GetAllText() { return inputField?inputField.text:nonUserInput; }
- /// <param name="text">Text to add as output, also turning current user input into text output</param>
- public void AddText(string text){ SetText (GetAllText () + text); }
- /// <param name="text">line to add as output, also turning current user input into text output</param>
- public void println(string line) { SetText (GetAllText () + line + "\n"); }
- public void readLineAsync(DoAfterStringIsRead stringCallback) {
- if (!IsVisible ()) { SetVisibility (true); }
- waitingToReadLine += stringCallback;
- }
- /// <summary>Instance.println(line)</summary>
- public static void Log(string line) { Instance.println (line); }
- public static void ReadLine(DoAfterStringIsRead stringCallback) { Instance.readLineAsync(stringCallback); }
- /// <returns>The user input, which is text that the user has entered (at the bottom)</returns>
- public string GetUserInput() {
- string s = inputField.text;
- int len = s.Length - nonUserInput.Length;
- if (len > 0) {
- recentUserInput = (len > 0) ? s.Substring (nonUserInput.Length, len) : "";
- return recentUserInput;
- } else return "";
- }
- public void Run(string commandWithArguments) {
- if (commandWithArguments == null || commandWithArguments.Length == 0) { return; }
- string s = commandWithArguments.Trim (whitespace); // cut leading & trailing whitespace
- string[] args = parseArguments(s);
- if (args.Length < 1) { return; }
- if (onCommand != null) { onCommand (args); }
- Run(args[0].ToLower(), args);
- }
- /// <param name="command">Command.</param>
- /// <param name="args">Arguments. [0] is the name of the command, with [1] and beyond being the arguments</param>
- public void Run(string command, string[] args) {
- Command cmd = null;
- // try to find the given command. or the default command. if we can't find either...
- if (!commands.TryGetValue(command, out cmd) && !commands.TryGetValue ("", out cmd)) {
- // error!
- string error = "Unknown command '" + command + "'";
- if (args.Length > 1) { error += " with arguments "; }
- for (int i = 1; i < args.Length; ++i) {
- if (i > 1) { error += ", "; }
- error += "'"+args [i]+"'";
- }
- Log(error);
- }
- // if we have a command
- if (cmd != null) {
- // execute it if it has valid code
- if (cmd.handler != null) {
- cmd.handler (args);
- } else {
- Log("Null command '" + command + "'");
- }
- }
- }
- /// <summary>execute the current input as a command</summary>
- public void Run() { Run(GetUserInput()); }
- private static readonly char[] quotes = new char[]{'\'','\"'},
- whitespace = new char[]{' ','\t','\n', '\b', '\r'};
- /// <returns>index of the end of the token that starts at the given index 'i'</returns>
- public static int FindEndToken(string str, int i) {
- bool isWhitespace;
- do {
- isWhitespace = System.Array.IndexOf(whitespace, str [i]) >= 0;
- if(isWhitespace) { ++i; }
- } while(isWhitespace && i < str.Length);
- int index = System.Array.IndexOf(quotes, str [i]);
- char startQuote = (index >= 0)?quotes[index]:'\0';
- if (startQuote != '\0') { ++i; }
- while (i < str.Length) {
- if (startQuote != '\0') {
- if (str [i] == '\\') {
- i++; // skip the next character for an escape sequence. just leave it there.
- } else {
- index = System.Array.IndexOf(quotes, str [i]);
- bool endsQuote = index >= 0 && quotes [index] == startQuote;
- if (endsQuote) { i++; break; }
- }
- } else {
- isWhitespace = System.Array.IndexOf(whitespace, str [i]) >= 0;
- if (isWhitespace) { break; }
- }
- i++;
- }
- if (i >= str.Length) { i = str.Length; }
- return i;
- }
- /// <returns>split command-line arguments</returns>
- static string[] parseArguments(string commandLineInput) {
- int index = 0;
- string token;
- List<string> tokens = new List<string> ();
- while (index < commandLineInput.Length) {
- int end = FindEndToken (commandLineInput, index);
- if (index != end) {
- token = commandLineInput.Substring (index, end - index).TrimStart(whitespace);
- token = Unescape (token);
- int qi = System.Array.IndexOf(quotes, token [0]);
- if (qi >= 0 && token [token.Length - 1] == quotes [qi]) {
- token = token.Substring (1, token.Length - 2);
- }
- tokens.Add (token);
- }
- index = end;
- }
- return tokens.ToArray ();
- }
- /* https://msdn.microsoft.com/en-us/library/aa691087(v=vs.71).aspx */
- private readonly static SortedDictionary<char, char> EscapeMap = new SortedDictionary<char, char> {
- {'0','\0'}, {'a','\a'}, {'b','\b'}, {'f','\f'}, {'n','\n'}, {'r','\r'}, {'t','\t'}, {'v','\v'},
- };
- public static string Unescape(string escaped) {
- if (escaped == null) { return escaped; }
- StringBuilder sb = new StringBuilder();
- bool inEscape = false;
- int startIndex = 0;
- for (int i = 0; i < escaped.Length; i++) {
- if (!inEscape) {
- inEscape = escaped[i] == '\\';
- } else {
- char c;
- if (!EscapeMap.TryGetValue(escaped[i], out c)) {
- c = escaped [i]; // unknown escape sequences are literals
- }
- sb.Append(escaped.Substring(startIndex, i - startIndex - 1));
- sb.Append(c);
- startIndex = i + 1;
- inEscape = false;
- }
- }
- sb.Append(escaped.Substring(startIndex));
- return sb.ToString();
- }
- [Tooltip("Which key toggles (hides/shows) the UI for this object.")]
- public KeyCode toggleKey = KeyCode.BackQuote;
- [Tooltip("ScrollRect container. If null, one will be created, with appropriate settings.")]
- public ScrollRect scrollView;
- [Serializable]
- public class RectTransformSettings {
- public Vector2 anchorMin = Vector2.zero, anchorMax = Vector2.one, offsetMin = Vector2.zero, offsetMax = Vector2.zero;
- }
- [Tooltip("used to size the console Rect Transform on creation")]
- public RectTransformSettings initialRectTransformSettings;
- [Tooltip("InputField element. If null, one will be created, with appropriate settings.")]
- public InputField inputField;
- [Tooltip("The font used. If null, \'Arial.ttf\' (built-in font) will be used.")]
- public Font font;
- [Tooltip("Maximum number of lines to retain.")]
- public int maxLines = 99;
- [Tooltip("lines with more characters than this will count as more than one line.")]
- public int maxColumnsPerLine = 120;
- /// <summary>used to prevent multiple simultaneous toggles of visibility</summary>
- private bool togglingVisiblity = false;
- /// <summary>flag that disables consistency checks, possibly preventing stack-overflow</summary>
- private bool sudoAdjustment = false;
- /// <summary>flag to check if the caret (text focus indicator) has been properly parented</summary>
- private bool inputCaretMoved = false;
- /// <summary>flag to deselect all text if newly activated</summary>
- private bool shouldDeselectAllBecauseOfNewVisibility = false;
- /// <summary>flag to move text view to the bottom when content is added</summary>
- private bool showBottomWhenTextIsAdded = false;
- [Tooltip("If true, will show up immediately")]
- public bool visibleOnStart = true;
- #region debug log intercept
- [SerializeField, Tooltip("If true, all Debug.Log messages will be intercepted and duplicated here.")]
- private bool interceptDebugLog = true;
- /// <summary>if this object was intercepting Debug.Logs, this will ensure that it un-intercepts as needed</summary>
- private bool dbgIntercepted = false;
- /// <summary>what to do after a string is read.</summary>
- public delegate void DoAfterStringIsRead(string readFromUser);
- /// <summary>if delegates are here, calls this code instead of executing a known a command</summary>
- private event DoAfterStringIsRead waitingToReadLine;
- /// <summary>If this is set, ignore the native CommandLine functionality, and just do this</summary>
- public event DoAfterStringIsRead onInput;
- void OnEnable() { SetDebugIntercept (interceptDebugLog); }
- void OnDisable() { SetDebugIntercept (false); }
- public void SetDebugIntercept(bool intercept) {
- if (intercept && !dbgIntercepted) { Application.logMessageReceived += HandleLog; dbgIntercepted = true; }
- else if (!intercept && dbgIntercepted) { Application.logMessageReceived -= HandleLog; dbgIntercepted = false; }
- }
- void HandleLog(string logString, string stackTrace, LogType type) { Log(logString); }
- #endregion
- /// <summary>the singleton instance. One will be created if none exist.</summary>
- private static CommandLine instance;
- public static CommandLine Instance {
- get {
- if (instance == null) {
- if ((instance = FindObjectOfType (typeof(CommandLine)) as CommandLine) == null) {
- GameObject g = new GameObject ();
- instance = g.AddComponent<CommandLine> ();
- g.name = "<" + instance.GetType ().Name + ">";
- }
- }
- return instance;
- }
- }
- /// <summary>Convenience method. Finds the component here, or in a parent object.</summary>
- public static T FindComponentUpHierarchy<T>(Transform t) where T : Component {
- T found = null;
- while (t != null && found == null) { found = t.GetComponent<T> (); t = t.parent; }
- return found;
- }
- /// <param name="layer">what Unity layer to set the given object, and all child objects, recursive</param>
- public static void SetLayerRecursive(GameObject go, int layer) {
- go.layer = layer;
- for(int i=0;i<go.transform.childCount;++i){
- Transform t = go.transform.GetChild(i);
- if(t != null) {
- SetLayerRecursive(t.gameObject, layer);
- }
- }
- }
- void Start() {
- CreateUI ();
- inputField.onValueChanged.AddListener((str) => {
- char lastAdded = '\0';
- if (inputField.caretPosition < nonUserInput.Length) {
- lastAdded = RefreshText();
- }
- bool letterWasAdded = str.Length > nonUserInput.Length;
- if(letterWasAdded) {
- if(lastAdded == '\0') {
- lastAdded = str [str.Length - 1];
- }
- if (lastAdded == '\n') {
- if(waitingToReadLine != null) {
- waitingToReadLine(GetUserInput());
- waitingToReadLine = null;
- } else if(onInput != null) {
- onInput(GetUserInput());
- } else {
- Run();
- }
- SetText(inputField.text);
- }
- scrollView.verticalNormalizedPosition = 0;
- showBottomWhenTextIsAdded = true;
- }
- if (inputField.caretPosition < nonUserInput.Length) {
- inputField.caretPosition = nonUserInput.Length;
- }
- });
- // test code
- PopulateWithBasicCommands ();
- if (nonUserInput.Length == 0) { Log(Application.productName + ", v" + Application.version); }
- }
- public GameObject CreateUI() {
- Canvas c = FindComponentUpHierarchy<Canvas> (transform);
- if (!c) {
- GameObject CANVAS = new GameObject ("canvas");
- c = CANVAS.AddComponent<Canvas> (); // so that the UI can be drawn at all
- c.renderMode = RenderMode.ScreenSpaceOverlay;
- if(!CANVAS.GetComponent<CanvasScaler>()){
- CANVAS.AddComponent<CanvasScaler> (); // so that text is pretty when zoomed
- }
- if(!CANVAS.GetComponent<GraphicRaycaster> ()){
- CANVAS.AddComponent<GraphicRaycaster> (); // so that mouse can select input area
- }
- CANVAS.transform.SetParent (transform);
- }
- if (!scrollView) {
- GameObject scrollViewO = new GameObject ("scrollview");
- scrollViewO.transform.SetParent (c.transform);
- Image img = scrollViewO.AddComponent<Image> ();
- img.color = new Color (0, 0, 0, 0.5f);
- scrollView = scrollViewO.AddComponent<ScrollRect> ();
- scrollView.verticalScrollbarVisibility = ScrollRect.ScrollbarVisibility.AutoHideAndExpandViewport;
- scrollView.verticalScrollbarSpacing = -3;
- scrollView.horizontal = false;
- scrollView.movementType = ScrollRect.MovementType.Clamped;
- if (initialRectTransformSettings == null) {
- MaximizeRectTransform (scrollView.transform);
- } else {
- RectTransform r = scrollView.GetComponent<RectTransform> ();
- r.anchorMin = initialRectTransformSettings.anchorMin;
- r.anchorMax = initialRectTransformSettings.anchorMax;
- r.offsetMin = initialRectTransformSettings.offsetMin;
- r.offsetMax = initialRectTransformSettings.offsetMax;
- }
- }
- if (scrollView.viewport == null) {
- GameObject viewport = new GameObject ("viewport");
- viewport.transform.SetParent (scrollView.transform);
- Image img = viewport.AddComponent<Image> ();
- img.color = new Color(0,0,0,0.5f);
- viewport.AddComponent<Mask> ();
- RectTransform r = MaximizeRectTransform (viewport.transform);
- r.offsetMax = new Vector2 (-16, 0);
- r.pivot = new Vector2 (0, 1);
- scrollView.viewport = r;
- }
- if (scrollView.content == null) {
- GameObject content = new GameObject ("content");
- Text text = content.AddComponent<Text> ();
- text.transform.SetParent (scrollView.viewport.transform);
- if (font == null) {
- font = Resources.GetBuiltinResource<Font> ("Arial.ttf");
- }
- text.font = font;
- text.supportRichText = false;
- text.verticalOverflow = VerticalWrapMode.Overflow;
- RectTransform r = content.GetComponent<RectTransform>();
- r.anchorMin = new Vector2 (0, 1);
- r.anchorMax = Vector2.one;
- r.offsetMin = r.offsetMax = Vector2.zero;
- r.pivot = new Vector2 (0, 1); // scroll from the top
- inputField = content.AddComponent<InputField> ();
- inputField.lineType = InputField.LineType.MultiLineNewline;
- inputField.textComponent = text;
- inputField.caretWidth = 5;
- ContentSizeFitter csf = content.AddComponent<ContentSizeFitter> ();
- csf.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
- scrollView.content = r;
- } else if (font != null) {
- inputField.textComponent.font = font;
- }
- if (scrollView.verticalScrollbar == null) {
- GameObject scrollbar = new GameObject ("scrollbar vertical");
- scrollbar.transform.SetParent (scrollView.transform);
- scrollbar.AddComponent<RectTransform> ();
- scrollView.verticalScrollbar = scrollbar.AddComponent<Scrollbar> ();
- scrollView.verticalScrollbar.direction = Scrollbar.Direction.BottomToTop;
- RectTransform r = scrollbar.GetComponent<RectTransform> ();
- r.anchorMin = new Vector2(1, 0);
- r.anchorMax = Vector2.one;
- r.offsetMax = Vector3.zero;
- r.offsetMin = new Vector2 (-16, 0);
- }
- if (scrollView.verticalScrollbar.handleRect == null) {
- GameObject slideArea = new GameObject ("sliding area");
- slideArea.transform.SetParent (scrollView.verticalScrollbar.transform);
- RectTransform r = slideArea.AddComponent<RectTransform> ();
- MaximizeRectTransform (slideArea.transform);
- r.offsetMin = new Vector2(10, 10);
- r.offsetMax = new Vector2(-10, -10);
- GameObject handle = new GameObject ("handle");
- Image img = handle.AddComponent<Image> ();
- img.color = new Color (1, 1, 1, 0.5f);
- handle.transform.SetParent (slideArea.transform);
- r = handle.GetComponent<RectTransform> ();
- r.anchorMin = r.anchorMax = Vector2.zero;
- r.offsetMax = new Vector2 (5, 5);
- r.offsetMin = new Vector2 (-5, -5);
- r.pivot = Vector2.one;
- scrollView.verticalScrollbar.handleRect = r;
- scrollView.verticalScrollbar.targetGraphic = img;
- }
- // an event system is required... if there isn't one, make one
- StandaloneInputModule input = FindObjectOfType(typeof(StandaloneInputModule)) as StandaloneInputModule;
- if (input == null) { input = (new GameObject ("<EventSystem>")).AddComponent<StandaloneInputModule> (); }
- // put all UI in the UI layer
- SetLayerRecursive (c.gameObject, LayerMask.NameToLayer ("UI"));
- RefreshText ();
- shouldDeselectAllBecauseOfNewVisibility = true;
- // force visibliity recalculations after UI changes
- scrollView.content.gameObject.SetActive(false);
- scrollView.content.gameObject.SetActive(true);
- SetVisibility (visibleOnStart);
- return scrollView.gameObject;
- }
- private static RectTransform MaximizeRectTransform(Transform t) {
- return MaximizeRectTransform(t.GetComponent<RectTransform>());
- }
- private static RectTransform MaximizeRectTransform(RectTransform r) {
- r.anchorMax = Vector2.one;
- r.anchorMin = r.offsetMin = r.offsetMax = Vector2.zero;
- return r;
- }
- void Update() {
- // toggle visibility on keypress
- if (Input.GetKeyDown(toggleKey)) {
- // and if the keypress showed up in the inputfield, remove it
- string t = inputField.text;
- if (inputField.caretPosition > nonUserInput.Length) {
- char lastChar = t [inputField.caretPosition - 1];
- if (Input.GetKey(lastChar.ToString())) {
- inputField.text = t.Substring (0, inputField.caretPosition - 1) +
- t.Substring(inputField.caretPosition);
- }
- }
- ToggleVisibility();
- }
- //Toggle visibility when 5 fingers touch.
- if (Input.touches.Length == 5) {
- if (!togglingVisiblity) {
- ToggleVisibility();
- togglingVisiblity = true;
- }
- } else {
- togglingVisiblity = false;
- }
- if (inputField.text.Length <= nonUserInput.Length) {
- RefreshText ();
- }
- // code to fix the input caret, so that it scrolls correctly
- if (!inputCaretMoved) {
- Transform p = inputField.textComponent.transform.parent;
- for (int i = 0; i < p.childCount; ++i) {
- if (p.GetChild (i).name.EndsWith ("Input Caret")) {
- Transform caret = p.GetChild (i);
- caret.transform.SetParent (inputField.textComponent.transform);
- inputCaretMoved = true;
- break;
- }
- }
- }
- if (shouldDeselectAllBecauseOfNewVisibility
- && inputField.selectionAnchorPosition == inputField.text.Length && inputField.selectionFocusPosition == 0) {
- MoveCaretToEnd ();
- shouldDeselectAllBecauseOfNewVisibility = false;
- }
- if (Input.GetAxis ("Mouse ScrollWheel") != 0) {
- showBottomWhenTextIsAdded = scrollView.verticalNormalizedPosition == 0;
- }
- if (showBottomWhenTextIsAdded) { scrollView.verticalNormalizedPosition = 0; }
- }
- /// <summary>If shown, hide. If hidden, show.</summary>
- void ToggleVisibility() { SetVisibility(!scrollView.gameObject.activeInHierarchy); }
- /// <summary>Moves the caret to the end, clearing all selections in the process</summary>
- public void MoveCaretToEnd() {
- int lastPoint = inputField.text.Length;
- inputField.caretPosition = lastPoint;
- }
- public bool IsVisible() {
- return scrollView != null && scrollView.gameObject.activeInHierarchy;
- }
- /// <summary>shows (true) or hides (false).</summary>
- public void SetVisibility(bool visible) {
- if (scrollView == null) {
- visibleOnStart = visible;
- } else {
- scrollView.gameObject.SetActive (visible);
- if (visible) {
- scrollView.verticalNormalizedPosition = 0;
- inputField.ActivateInputField ();
- shouldDeselectAllBecauseOfNewVisibility = true;
- } else {
- MoveCaretToEnd ();
- }
- }
- }
- }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement