HeavenStudioPlus/Assets/Scripts/LevelEditor/Editor.cs

555 lines
20 KiB
C#
Raw Normal View History

2022-01-30 12:03:37 +00:00
using System;
using System.IO;
2022-01-03 22:42:43 +00:00
using System.Collections;
using System.Collections.Generic;
2022-01-30 12:03:37 +00:00
using System.Threading.Tasks;
2022-01-03 22:42:43 +00:00
2022-01-30 12:03:37 +00:00
using UnityEngine;
2022-01-03 22:42:43 +00:00
using UnityEngine.UI;
2022-01-30 12:03:37 +00:00
using UnityEngine.Networking;
2022-01-03 22:42:43 +00:00
using Newtonsoft.Json;
using TMPro;
using Starpelly;
2022-01-30 12:03:37 +00:00
using SFB;
using HeavenStudio.Common;
2022-03-14 14:21:05 +00:00
using HeavenStudio.Editor.Track;
using HeavenStudio.Util;
using HeavenStudio.StudioDance;
2022-01-30 12:03:37 +00:00
using System.IO.Compression;
using System.Text;
2022-03-14 14:21:05 +00:00
namespace HeavenStudio.Editor
2022-01-03 22:42:43 +00:00
{
public class Editor : MonoBehaviour
{
private GameInitializer Initializer;
2022-01-03 22:42:43 +00:00
2022-06-28 19:57:32 +00:00
[SerializeField] public Canvas MainCanvas;
[SerializeField] public Camera EditorCamera;
2022-01-06 00:11:33 +00:00
2022-01-03 22:42:43 +00:00
[Header("Rect")]
[SerializeField] private RenderTexture ScreenRenderTexture;
[SerializeField] private RawImage Screen;
2022-01-09 23:35:55 +00:00
[SerializeField] private RectTransform GridGameSelector;
public RectTransform eventSelectorBG;
2022-01-03 22:42:43 +00:00
2022-01-06 00:11:33 +00:00
[Header("Components")]
[SerializeField] private Timeline Timeline;
[SerializeField] private TMP_Text GameEventSelectorTitle;
[SerializeField] private TMP_Text BuildDateDisplay;
[SerializeField] public StudioDanceManager StudioDanceManager;
2022-01-06 00:11:33 +00:00
2022-01-15 07:08:23 +00:00
[Header("Toolbar")]
[SerializeField] private Button NewBTN;
[SerializeField] private Button OpenBTN;
[SerializeField] private Button SaveBTN;
[SerializeField] private Button UndoBTN;
[SerializeField] private Button RedoBTN;
2022-01-29 21:59:20 +00:00
[SerializeField] private Button MusicSelectBTN;
[SerializeField] private Button FullScreenBTN;
2022-02-28 08:38:43 +00:00
[SerializeField] private Button TempoFinderBTN;
2022-06-30 01:58:21 +00:00
[SerializeField] private Button SnapDiagBTN;
[SerializeField] private Button ChartParamBTN;
2022-01-15 07:08:23 +00:00
2022-07-04 16:57:19 +00:00
[SerializeField] private Button EditorThemeBTN;
[SerializeField] private Button EditorSettingsBTN;
[Header("Dialogs")]
[SerializeField] private Dialog[] Dialogs;
[Header("Tooltip")]
public TMP_Text tooltipText;
2022-01-30 12:03:37 +00:00
[Header("Properties")]
private bool changedMusic = false;
2022-02-03 22:20:26 +00:00
private bool loadedMusic = false;
2022-01-30 12:03:37 +00:00
private string currentRemixPath = "";
2022-02-04 22:16:22 +00:00
private string remixName = "";
2022-07-10 21:39:14 +00:00
public bool fullscreen;
2022-02-03 02:09:50 +00:00
public bool discordDuringTesting = false;
public bool canSelect = true;
public bool editingInputField = false;
public bool inAuthorativeMenu = false;
2022-07-10 21:39:14 +00:00
public bool isCursorEnabled = true;
public bool isDiscordEnabled = true;
2022-01-12 03:29:27 +00:00
public bool isShortcutsEnabled { get { return (!inAuthorativeMenu) && (!editingInputField); } }
private byte[] MusicBytes;
public static Editor instance { get; private set; }
2022-01-03 22:42:43 +00:00
private void Start()
{
instance = this;
Initializer = GetComponent<GameInitializer>();
canSelect = true;
2022-01-06 00:11:33 +00:00
}
public void Init()
{
Game Overlays (#280) * add accuracy display * temp BG for show * separate overlays prefab make proper shader for star effects * aim shakiness display * implement testing skill star * fully functional skill star * separate section display from editor * fully separate chart section display from timeline * add section to overlays * fix nullreference issues * start game layout settings * add game settings script * fix nonfunctioning scoring * invert y position logic on timing bar * add perfect challenge functionality * fix section not showing up in editor add perfect challenge option * add timing display minimal mode * Update PerfectAndPractice.png * show gismo for minigame bounds in editor * add ability to disable overlays in editor * prepare medals add new timing display graphic * hide screen preview * per-axis camera control added per request * section medals basic functionality * add medal get animations * fix bug with perfect icons * visual enhancements * adjust look of timing display minmode address audio ducking issues(?) * prepare overlay lyt editor add viewport pan, rotate, scale adjust audio setting * add layout editor UI elements * dynamic overlay creation * fix default single timing disp * set up overlay settings controls * start UI events * runtime uuid for component reference * layout editor affects overlay elements * show overlay element previews while editing * advanced audio settings * fix bug in drop-down creation * fallback defaults for the new stuff * fix textbox & overlay visibility bugs
2023-03-11 04:51:22 +00:00
GameManager.instance.StaticCamera.targetTexture = ScreenRenderTexture;
2022-01-03 22:42:43 +00:00
GameManager.instance.CursorCam.targetTexture = ScreenRenderTexture;
Screen.texture = ScreenRenderTexture;
2022-01-06 00:11:33 +00:00
2022-01-08 16:42:48 +00:00
GameManager.instance.Init();
2022-01-06 00:11:33 +00:00
Timeline.Init();
2022-01-09 23:35:55 +00:00
foreach (var minigame in EventCaller.instance.minigames)
AddIcon(minigame);
2022-01-11 02:06:13 +00:00
2022-01-23 03:40:53 +00:00
Tooltip.AddTooltip(NewBTN.gameObject, "New <color=#adadad>[Ctrl+N]</color>");
Tooltip.AddTooltip(OpenBTN.gameObject, "Open <color=#adadad>[Ctrl+O]</color>");
Tooltip.AddTooltip(SaveBTN.gameObject, "Save Project <color=#adadad>[Ctrl+S]</color>\nSave Project As <color=#adadad>[Ctrl+Alt+S]</color>");
Tooltip.AddTooltip(UndoBTN.gameObject, "Undo <color=#adadad>[Ctrl+Z]</color>");
Tooltip.AddTooltip(RedoBTN.gameObject, "Redo <color=#adadad>[Ctrl+Y or Ctrl+Shift+Z]</color>");
2022-01-29 21:59:20 +00:00
Tooltip.AddTooltip(MusicSelectBTN.gameObject, "Music Select");
Tooltip.AddTooltip(FullScreenBTN.gameObject, "Preview <color=#adadad>[Tab]</color>");
2022-02-28 08:38:43 +00:00
Tooltip.AddTooltip(TempoFinderBTN.gameObject, "Tempo Finder");
2022-06-30 01:58:21 +00:00
Tooltip.AddTooltip(SnapDiagBTN.gameObject, "Snap Settings");
Tooltip.AddTooltip(ChartParamBTN.gameObject, "Remix Properties");
2022-01-30 23:40:12 +00:00
2022-07-04 16:57:19 +00:00
Tooltip.AddTooltip(EditorSettingsBTN.gameObject, "Editor Settings <color=#adadad>[Ctrl+Shift+O]</color>");
2022-01-30 23:40:12 +00:00
UpdateEditorStatus(true);
BuildDateDisplay.text = GlobalGameManager.buildTime;
isCursorEnabled = PersistentDataManager.gameSettings.editorCursorEnable;
isDiscordEnabled = PersistentDataManager.gameSettings.discordRPCEnable;
GameManager.instance.CursorCam.enabled = isCursorEnabled;
2022-01-09 23:35:55 +00:00
}
public void AddIcon(Minigames.Minigame minigame)
{
if (minigame.hidden) return;
GameObject GameIcon_ = Instantiate(GridGameSelector.GetChild(0).gameObject, GridGameSelector);
GameIcon_.GetComponent<Image>().sprite = GameIcon(minigame.name);
GameIcon_.GetComponent<GridGameSelectorGame>().MaskTex = GameIconMask(minigame.name);
GameIcon_.GetComponent<GridGameSelectorGame>().UnClickIcon();
GameIcon_.gameObject.SetActive(true);
GameIcon_.name = minigame.displayName;
}
2022-02-03 07:28:14 +00:00
public void LateUpdate()
2022-01-14 22:46:14 +00:00
{
2022-07-02 02:03:15 +00:00
#region Keyboard Shortcuts
if (isShortcutsEnabled)
{
2022-07-02 02:03:15 +00:00
if (Input.GetKeyDown(KeyCode.Tab))
{
Fullscreen();
2022-07-02 02:03:15 +00:00
}
if (Input.GetKeyDown(KeyCode.Delete) || Input.GetKeyDown(KeyCode.Backspace))
{
List<TimelineEventObj> ev = new List<TimelineEventObj>();
for (int i = 0; i < Selections.instance.eventsSelected.Count; i++) ev.Add(Selections.instance.eventsSelected[i]);
CommandManager.instance.Execute(new Commands.Deletion(ev));
}
2022-07-02 02:03:15 +00:00
if (Input.GetKey(KeyCode.LeftControl))
{
if (Input.GetKeyDown(KeyCode.Z))
{
if (Input.GetKey(KeyCode.LeftShift))
CommandManager.instance.Redo();
else
CommandManager.instance.Undo();
}
else if (Input.GetKeyDown(KeyCode.Y))
{
CommandManager.instance.Redo();
}
if (Input.GetKey(KeyCode.LeftShift))
{
if (Input.GetKeyDown(KeyCode.D))
{
ToggleDebugCam();
}
}
}
if (Input.GetKey(KeyCode.LeftControl))
{
if (Input.GetKeyDown(KeyCode.N))
{
NewBTN.onClick.Invoke();
2022-07-02 02:03:15 +00:00
}
else if (Input.GetKeyDown(KeyCode.O))
{
OpenRemix();
}
else if (Input.GetKey(KeyCode.LeftAlt))
{
if (Input.GetKeyDown(KeyCode.S))
{
SaveRemix(true);
}
}
else if (Input.GetKeyDown(KeyCode.S))
{
SaveRemix(false);
}
}
}
2022-07-02 02:03:15 +00:00
#endregion
if (CommandManager.instance.canUndo())
UndoBTN.transform.GetChild(0).GetComponent<Image>().color = Color.white;
else
UndoBTN.transform.GetChild(0).GetComponent<Image>().color = Color.gray;
if (CommandManager.instance.canRedo())
RedoBTN.transform.GetChild(0).GetComponent<Image>().color = Color.white;
else
RedoBTN.transform.GetChild(0).GetComponent<Image>().color = Color.gray;
if (Timeline.instance.timelineState.selected && Editor.instance.canSelect)
{
2022-02-03 22:20:26 +00:00
if (Input.GetMouseButtonUp(0))
{
2022-02-03 22:20:26 +00:00
List<TimelineEventObj> selectedEvents = Timeline.instance.eventObjs.FindAll(c => c.selected == true && c.eligibleToMove == true);
2022-02-03 22:20:26 +00:00
if (selectedEvents.Count > 0)
{
2022-02-03 22:20:26 +00:00
List<TimelineEventObj> result = new List<TimelineEventObj>();
for (int i = 0; i < selectedEvents.Count; i++)
{
//TODO: this is in LateUpdate, so this will never run! change this to something that works properly
if (!(selectedEvents[i].isCreating || selectedEvents[i].wasDuplicated))
2022-02-03 22:20:26 +00:00
{
result.Add(selectedEvents[i]);
}
selectedEvents[i].OnUp();
}
2022-02-03 22:20:26 +00:00
CommandManager.instance.Execute(new Commands.Move(result));
}
}
}
2022-01-14 22:46:14 +00:00
}
2022-01-09 23:35:55 +00:00
public static Sprite GameIcon(string name)
{
return Resources.Load<Sprite>($"Sprites/Editor/GameIcons/{name}");
2022-01-03 22:42:43 +00:00
}
2022-01-15 22:52:53 +00:00
public static Texture GameIconMask(string name)
{
return Resources.Load<Texture>($"Sprites/Editor/GameIcons/{name}_mask");
}
2022-01-30 12:03:37 +00:00
#region Dialogs
public void SelectMusic()
{
var extensions = new[]
{
new ExtensionFilter("Music Files", "mp3", "ogg", "wav")
};
#if UNITY_STANDALONE_WINDOWS
2022-01-30 12:03:37 +00:00
StandaloneFileBrowser.OpenFilePanelAsync("Open File", "", extensions, false, async (string[] paths) =>
{
if (paths.Length > 0)
{
Conductor.instance.musicSource.clip = await LoadClip(Path.Combine(paths));
changedMusic = true;
Timeline.FitToSong();
2022-01-30 12:03:37 +00:00
}
}
);
#else
StandaloneFileBrowser.OpenFilePanelAsync("Open File", "", extensions, false, async (string[] paths) =>
{
if (paths.Length > 0)
{
Conductor.instance.musicSource.clip = await LoadClip("file://" + Path.Combine(paths));
changedMusic = true;
Timeline.FitToSong();
Timeline.CreateWaveform();
}
}
);
#endif
2022-01-30 12:03:37 +00:00
}
private async Task<AudioClip> LoadClip(string path)
{
AudioClip clip = null;
AudioType audioType = AudioType.OGGVORBIS;
// this is a bad solution but i'm lazy
if (path.Substring(path.Length - 3) == "ogg")
audioType = AudioType.OGGVORBIS;
else if (path.Substring(path.Length - 3) == "mp3")
audioType = AudioType.MPEG;
else if (path.Substring(path.Length - 3) == "wav")
audioType = AudioType.WAV;
using (UnityWebRequest uwr = UnityWebRequestMultimedia.GetAudioClip(path, audioType))
{
uwr.SendWebRequest();
try
{
while (!uwr.isDone) await Task.Delay(5);
if (uwr.result == UnityWebRequest.Result.ProtocolError) Debug.Log($"{uwr.error}");
else
{
clip = DownloadHandlerAudioClip.GetContent(uwr);
}
}
catch (Exception err)
{
Debug.Log($"{err.Message}, {err.StackTrace}");
}
}
try
{
if (clip != null)
MusicBytes = OggVorbis.VorbisPlugin.GetOggVorbis(clip, 1);
else
{
MusicBytes = null;
Debug.LogWarning("Failed to load music file! The stream is currently empty.");
}
}
catch (System.ArgumentNullException)
{
clip = null;
MusicBytes = null;
Debug.LogWarning("Failed to load music file! The stream is currently empty.");
}
catch (System.ArgumentOutOfRangeException)
{
clip = null;
MusicBytes = null;
Debug.LogWarning("Failed to load music file! The stream is malformed.");
}
catch (System.ArgumentException)
{
clip = null;
MusicBytes = null;
Debug.LogWarning("Failed to load music file! Only 1 or 2 channels are supported!.");
}
2022-01-30 12:03:37 +00:00
return clip;
}
public void SaveRemix(bool saveAs = true)
{
if (saveAs == true)
{
SaveRemixFilePanel();
}
else
{
if (currentRemixPath == string.Empty)
{
SaveRemixFilePanel();
}
else
{
SaveRemixFile(currentRemixPath);
}
}
}
private void SaveRemixFilePanel()
{
var extensions = new[]
{
new ExtensionFilter("Heaven Studio Remix File", "riq")
2022-01-30 12:03:37 +00:00
};
2022-01-30 12:03:37 +00:00
StandaloneFileBrowser.SaveFilePanelAsync("Save Remix As", "", "remix_level", extensions, (string path) =>
{
if (path != String.Empty)
{
SaveRemixFile(path);
}
});
}
private void SaveRemixFile(string path)
{
using (FileStream zipFile = File.Open(path, FileMode.Create))
2022-01-30 12:03:37 +00:00
{
2022-02-03 22:20:26 +00:00
using (var archive = new ZipArchive(zipFile, ZipArchiveMode.Update))
2022-01-30 12:03:37 +00:00
{
var levelFile = archive.CreateEntry("remix.json", System.IO.Compression.CompressionLevel.NoCompression);
using (var zipStream = levelFile.Open())
zipStream.Write(Encoding.UTF8.GetBytes(GetJson()), 0, Encoding.UTF8.GetBytes(GetJson()).Length);
2022-01-30 12:03:37 +00:00
if (MusicBytes != null)
2022-01-30 12:03:37 +00:00
{
var musicFile = archive.CreateEntry("song.ogg", System.IO.Compression.CompressionLevel.NoCompression);
using (var zipStream = musicFile.Open())
zipStream.Write(MusicBytes, 0, MusicBytes.Length);
2022-01-30 12:03:37 +00:00
}
}
2022-02-04 22:16:22 +00:00
currentRemixPath = path;
UpdateEditorStatus(false);
2022-01-30 12:03:37 +00:00
}
}
public void NewRemix()
{
2022-09-04 03:36:08 +00:00
if (Timeline.instance != null)
Timeline.instance?.Stop(0);
else
GameManager.instance.Stop(0);
MusicBytes = null;
LoadRemix("");
}
public void LoadRemix(string json = "", string type = "riq")
{
GameManager.instance.LoadRemix(json, type);
Timeline.instance.LoadRemix();
// Timeline.instance.SpecialInfo.UpdateStartingBPMText();
// Timeline.instance.VolumeInfo.UpdateStartingVolumeText();
// Timeline.instance.SpecialInfo.UpdateOffsetText();
Timeline.FitToSong();
currentRemixPath = string.Empty;
}
2022-01-30 12:03:37 +00:00
public void OpenRemix()
{
var extensions = new[]
{
new ExtensionFilter("All Supported Files ", new string[] { "riq", "tengoku", "rhmania" }),
new ExtensionFilter("Heaven Studio Remix File ", new string[] { "riq" }),
new ExtensionFilter("Legacy Heaven Studio Remix ", new string[] { "tengoku", "rhmania" })
2022-01-30 12:03:37 +00:00
};
StandaloneFileBrowser.OpenFilePanelAsync("Open Remix", "", extensions, false, (string[] paths) =>
{
var path = Path.Combine(paths);
2022-08-19 01:28:05 +00:00
if (path == string.Empty) return;
loadedMusic = false;
string extension = path.GetExtension();
2022-08-19 01:28:05 +00:00
using var zipFile = File.Open(path, FileMode.Open);
using var archive = new ZipArchive(zipFile, ZipArchiveMode.Read);
2022-08-19 01:28:05 +00:00
foreach (var entry in archive.Entries)
switch (entry.Name)
2022-01-30 12:03:37 +00:00
{
2022-08-19 01:28:05 +00:00
case "remix.json":
2022-01-30 12:03:37 +00:00
{
2022-08-19 01:28:05 +00:00
using var stream = entry.Open();
using var reader = new StreamReader(stream);
LoadRemix(reader.ReadToEnd(), extension);
2022-08-19 01:28:05 +00:00
break;
}
case "song.ogg":
{
using var stream = entry.Open();
using var memoryStream = new MemoryStream();
stream.CopyTo(memoryStream);
MusicBytes = memoryStream.ToArray();
Conductor.instance.musicSource.clip = OggVorbis.VorbisPlugin.ToAudioClip(MusicBytes, "music");
2022-08-19 01:28:05 +00:00
loadedMusic = true;
Timeline.FitToSong();
break;
2022-01-30 12:03:37 +00:00
}
}
2022-08-19 01:28:05 +00:00
if (!loadedMusic)
{
2022-08-19 01:28:05 +00:00
Conductor.instance.musicSource.clip = null;
MusicBytes = null;
}
2022-08-19 01:28:05 +00:00
currentRemixPath = path;
remixName = Path.GetFileName(path);
UpdateEditorStatus(false);
CommandManager.instance.Clear();
Timeline.FitToSong();
Timeline.CreateWaveform();
2022-01-30 12:03:37 +00:00
});
}
#endregion
public void Fullscreen()
{
MainCanvas.gameObject.SetActive(fullscreen);
if (fullscreen == false)
{
MainCanvas.enabled = false;
EditorCamera.enabled = false;
Game Overlays (#280) * add accuracy display * temp BG for show * separate overlays prefab make proper shader for star effects * aim shakiness display * implement testing skill star * fully functional skill star * separate section display from editor * fully separate chart section display from timeline * add section to overlays * fix nullreference issues * start game layout settings * add game settings script * fix nonfunctioning scoring * invert y position logic on timing bar * add perfect challenge functionality * fix section not showing up in editor add perfect challenge option * add timing display minimal mode * Update PerfectAndPractice.png * show gismo for minigame bounds in editor * add ability to disable overlays in editor * prepare medals add new timing display graphic * hide screen preview * per-axis camera control added per request * section medals basic functionality * add medal get animations * fix bug with perfect icons * visual enhancements * adjust look of timing display minmode address audio ducking issues(?) * prepare overlay lyt editor add viewport pan, rotate, scale adjust audio setting * add layout editor UI elements * dynamic overlay creation * fix default single timing disp * set up overlay settings controls * start UI events * runtime uuid for component reference * layout editor affects overlay elements * show overlay element previews while editing * advanced audio settings * fix bug in drop-down creation * fallback defaults for the new stuff * fix textbox & overlay visibility bugs
2023-03-11 04:51:22 +00:00
GameManager.instance.StaticCamera.targetTexture = null;
GameManager.instance.CursorCam.enabled = false;
fullscreen = true;
}
else
{
MainCanvas.enabled = true;
EditorCamera.enabled = true;
Game Overlays (#280) * add accuracy display * temp BG for show * separate overlays prefab make proper shader for star effects * aim shakiness display * implement testing skill star * fully functional skill star * separate section display from editor * fully separate chart section display from timeline * add section to overlays * fix nullreference issues * start game layout settings * add game settings script * fix nonfunctioning scoring * invert y position logic on timing bar * add perfect challenge functionality * fix section not showing up in editor add perfect challenge option * add timing display minimal mode * Update PerfectAndPractice.png * show gismo for minigame bounds in editor * add ability to disable overlays in editor * prepare medals add new timing display graphic * hide screen preview * per-axis camera control added per request * section medals basic functionality * add medal get animations * fix bug with perfect icons * visual enhancements * adjust look of timing display minmode address audio ducking issues(?) * prepare overlay lyt editor add viewport pan, rotate, scale adjust audio setting * add layout editor UI elements * dynamic overlay creation * fix default single timing disp * set up overlay settings controls * start UI events * runtime uuid for component reference * layout editor affects overlay elements * show overlay element previews while editing * advanced audio settings * fix bug in drop-down creation * fallback defaults for the new stuff * fix textbox & overlay visibility bugs
2023-03-11 04:51:22 +00:00
GameManager.instance.StaticCamera.targetTexture = ScreenRenderTexture;
2022-07-10 21:39:14 +00:00
GameManager.instance.CursorCam.enabled = true && isCursorEnabled;
fullscreen = false;
GameCamera.instance.camera.rect = new Rect(0, 0, 1, 1);
GameManager.instance.CursorCam.rect = new Rect(0, 0, 1, 1);
GameManager.instance.OverlayCamera.rect = new Rect(0, 0, 1, 1);
2022-06-26 21:37:30 +00:00
EditorCamera.rect = new Rect(0, 0, 1, 1);
}
Timeline.AutoBtnUpdate();
}
2022-01-30 23:40:12 +00:00
private void UpdateEditorStatus(bool updateTime)
{
GlobalGameManager.UpdateDiscordStatus($"{remixName}", true, updateTime);
2022-01-30 23:40:12 +00:00
}
2022-01-30 12:03:37 +00:00
public string GetJson()
2022-01-15 22:52:53 +00:00
{
string json = string.Empty;
json = JsonConvert.SerializeObject(GameManager.instance.Beatmap);
2022-01-30 12:03:37 +00:00
return json;
2022-01-15 22:52:53 +00:00
}
public void SetGameEventTitle(string txt)
{
GameEventSelectorTitle.text = txt;
}
public static bool MouseInRectTransform(RectTransform rectTransform)
{
return (rectTransform.gameObject.activeSelf && RectTransformUtility.RectangleContainsScreenPoint(rectTransform, Input.mousePosition, Editor.instance.EditorCamera));
}
public void ToggleDebugCam()
{
var game = GameManager.instance.currentGameO;
if (game != null)
{
foreach(FreeCam c in game.GetComponentsInChildren<FreeCam>(true))
{
c.enabled = !c.enabled;
}
}
}
2022-01-03 22:42:43 +00:00
}
}