diff --git a/OpenRA.Mods.YR/OpenRA.Mods.YR.csproj b/OpenRA.Mods.YR/OpenRA.Mods.YR.csproj
index 08ffaf22..aca20339 100644
--- a/OpenRA.Mods.YR/OpenRA.Mods.YR.csproj
+++ b/OpenRA.Mods.YR/OpenRA.Mods.YR.csproj
@@ -110,6 +110,9 @@
+
+
+
diff --git a/OpenRA.Mods.YR/Widgets/Logic/MainMenuLogic.cs b/OpenRA.Mods.YR/Widgets/Logic/MainMenuLogic.cs
new file mode 100644
index 00000000..1115d7da
--- /dev/null
+++ b/OpenRA.Mods.YR/Widgets/Logic/MainMenuLogic.cs
@@ -0,0 +1,538 @@
+#region Copyright & License Information
+/*
+ * Copyright 2007-2018 The OpenRA Developers (see AUTHORS)
+ * This file is part of OpenRA, which is free software. It is made
+ * available to you under the terms of the GNU General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version. For more
+ * information, see COPYING.
+ */
+#endregion
+
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Net;
+using OpenRA.Primitives;
+using OpenRA.Widgets;
+
+namespace OpenRA.Mods.Common.Widgets.Logic
+{
+ [SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1203:ConstantsMustAppearBeforeFields",
+ Justification = "SystemInformation version should be defined next to the dictionary it refers to.")]
+ public class NewMainMenuLogic : ChromeLogic
+ {
+ protected enum MenuType { Main, Singleplayer, Extras, MapEditor, SystemInfoPrompt, None }
+
+ protected enum MenuPanel { None, Missions, Skirmish, Multiplayer, MapEditor, Replays }
+
+ protected MenuType menuType = MenuType.Main;
+ readonly Widget rootMenu;
+ readonly ScrollPanelWidget newsPanel;
+ readonly Widget newsTemplate;
+ readonly LabelWidget newsStatus;
+
+ // Update news once per game launch
+ static bool fetchedNews;
+
+ protected static MenuPanel lastGameState = MenuPanel.None;
+
+ bool newsOpen;
+
+ // Increment the version number when adding new stats
+ const int SystemInformationVersion = 3;
+ Dictionary> GetSystemInformation()
+ {
+ var lang = System.Globalization.CultureInfo.InstalledUICulture.TwoLetterISOLanguageName;
+ return new Dictionary>()
+ {
+ { "id", Pair.New("Anonymous ID", Game.Settings.Debug.UUID) },
+ { "platform", Pair.New("OS Type", Platform.CurrentPlatform.ToString()) },
+ { "os", Pair.New("OS Version", Environment.OSVersion.ToString()) },
+ { "x64", Pair.New("OS is 64 bit", Environment.Is64BitOperatingSystem.ToString()) },
+ { "x64process", Pair.New("Process is 64 bit", Environment.Is64BitProcess.ToString()) },
+ { "runtime", Pair.New(".NET Runtime", Platform.RuntimeVersion) },
+ { "gl", Pair.New("OpenGL Version", Game.Renderer.GLVersion) },
+ { "windowsize", Pair.New("Window Size", "{0}x{1}".F(Game.Renderer.Resolution.Width, Game.Renderer.Resolution.Height)) },
+ { "windowscale", Pair.New("Window Scale", Game.Renderer.WindowScale.ToString("F2", CultureInfo.InvariantCulture)) },
+ { "lang", Pair.New("System Language", lang) }
+ };
+ }
+
+ void SwitchMenu(MenuType type)
+ {
+ menuType = type;
+
+ // Update button mouseover
+ Game.RunAfterTick(Ui.ResetTooltips);
+ }
+
+ [ObjectCreator.UseCtor]
+ public NewMainMenuLogic(Widget widget, World world, ModData modData)
+ {
+ rootMenu = widget;
+ rootMenu.Get("VERSION_LABEL").Text = modData.Manifest.Metadata.Version;
+
+ // Menu buttons
+ var mainMenu = widget.Get("MAIN_MENU");
+ mainMenu.IsVisible = () => menuType == MenuType.Main;
+
+ mainMenu.Get("SINGLEPLAYER_BUTTON").OnClick = () => SwitchMenu(MenuType.Singleplayer);
+
+ mainMenu.Get("MULTIPLAYER_BUTTON").OnClick = OpenMultiplayerPanel;
+
+ mainMenu.Get("CONTENT_BUTTON").OnClick = () =>
+ {
+ // Switching mods changes the world state (by disposing it),
+ // so we can't do this inside the input handler.
+ Game.RunAfterTick(() =>
+ {
+ var content = modData.Manifest.Get();
+ Game.InitializeMod(content.ContentInstallerMod, new Arguments(new[] { "Content.Mod=" + modData.Manifest.Id }));
+ });
+ };
+
+ mainMenu.Get("SETTINGS_BUTTON").OnClick = () =>
+ {
+ SwitchMenu(MenuType.None);
+ Game.OpenWindow("SETTINGS_PANEL", new WidgetArgs
+ {
+ { "onExit", () => SwitchMenu(MenuType.Main) }
+ });
+ };
+
+ mainMenu.Get("EXTRAS_BUTTON").OnClick = () => SwitchMenu(MenuType.Extras);
+
+ mainMenu.Get("QUIT_BUTTON").OnClick = Game.Exit;
+
+ // Singleplayer menu
+ var singleplayerMenu = widget.Get("SINGLEPLAYER_MENU");
+ singleplayerMenu.IsVisible = () => menuType == MenuType.Singleplayer;
+
+ var missionsButton = singleplayerMenu.Get("MISSIONS_BUTTON");
+ missionsButton.OnClick = OpenMissionBrowserPanel;
+
+ var hasCampaign = modData.Manifest.Missions.Any();
+ var hasMissions = modData.MapCache
+ .Any(p => p.Status == MapStatus.Available && p.Visibility.HasFlag(MapVisibility.MissionSelector));
+
+ missionsButton.Disabled = !hasCampaign && !hasMissions;
+
+ singleplayerMenu.Get("SKIRMISH_BUTTON").OnClick = StartSkirmishGame;
+
+ singleplayerMenu.Get("BACK_BUTTON").OnClick = () => SwitchMenu(MenuType.Main);
+
+ // Extras menu
+ var extrasMenu = widget.Get("EXTRAS_MENU");
+ extrasMenu.IsVisible = () => menuType == MenuType.Extras;
+
+ extrasMenu.Get("REPLAYS_BUTTON").OnClick = OpenReplayBrowserPanel;
+
+ extrasMenu.Get("MUSIC_BUTTON").OnClick = () =>
+ {
+ SwitchMenu(MenuType.None);
+ Ui.OpenWindow("MUSIC_PANEL", new WidgetArgs
+ {
+ { "onExit", () => SwitchMenu(MenuType.Extras) },
+ { "world", world }
+ });
+ };
+
+ extrasMenu.Get("MAP_EDITOR_BUTTON").OnClick = () => SwitchMenu(MenuType.MapEditor);
+
+ var assetBrowserButton = extrasMenu.GetOrNull("ASSETBROWSER_BUTTON");
+ if (assetBrowserButton != null)
+ assetBrowserButton.OnClick = () =>
+ {
+ SwitchMenu(MenuType.None);
+ Game.OpenWindow("VXLBROWSER_PANEL", new WidgetArgs
+ {
+ { "onExit", () => SwitchMenu(MenuType.Extras) },
+ });
+ };
+
+ extrasMenu.Get("CREDITS_BUTTON").OnClick = () =>
+ {
+ SwitchMenu(MenuType.None);
+ Ui.OpenWindow("CREDITS_PANEL", new WidgetArgs
+ {
+ { "onExit", () => SwitchMenu(MenuType.Extras) },
+ });
+ };
+
+ extrasMenu.Get("BACK_BUTTON").OnClick = () => SwitchMenu(MenuType.Main);
+
+ // Map editor menu
+ var mapEditorMenu = widget.Get("MAP_EDITOR_MENU");
+ mapEditorMenu.IsVisible = () => menuType == MenuType.MapEditor;
+
+ // Loading into the map editor
+ Game.BeforeGameStart += RemoveShellmapUI;
+
+ var onSelect = new Action(uid => LoadMapIntoEditor(modData.MapCache[uid].Uid));
+
+ var newMapButton = widget.Get("NEW_MAP_BUTTON");
+ newMapButton.OnClick = () =>
+ {
+ SwitchMenu(MenuType.None);
+ Game.OpenWindow("NEW_MAP_BG", new WidgetArgs()
+ {
+ { "onSelect", onSelect },
+ { "onExit", () => SwitchMenu(MenuType.MapEditor) }
+ });
+ };
+
+ var loadMapButton = widget.Get("LOAD_MAP_BUTTON");
+ loadMapButton.OnClick = () =>
+ {
+ SwitchMenu(MenuType.None);
+ Game.OpenWindow("MAPCHOOSER_PANEL", new WidgetArgs()
+ {
+ { "initialMap", null },
+ { "initialTab", MapClassification.User },
+ { "onExit", () => SwitchMenu(MenuType.MapEditor) },
+ { "onSelect", onSelect },
+ { "filter", MapVisibility.Lobby | MapVisibility.Shellmap | MapVisibility.MissionSelector },
+ });
+ };
+
+ mapEditorMenu.Get("BACK_BUTTON").OnClick = () => SwitchMenu(MenuType.Extras);
+
+ var newsBG = widget.GetOrNull("NEWS_BG");
+ if (newsBG != null)
+ {
+ newsBG.IsVisible = () => Game.Settings.Game.FetchNews && menuType != MenuType.None && menuType != MenuType.SystemInfoPrompt;
+
+ newsPanel = Ui.LoadWidget("NEWS_PANEL", null, new WidgetArgs());
+ newsTemplate = newsPanel.Get("NEWS_ITEM_TEMPLATE");
+ newsPanel.RemoveChild(newsTemplate);
+
+ newsStatus = newsPanel.Get("NEWS_STATUS");
+ SetNewsStatus("Loading news");
+ }
+
+ Game.OnRemoteDirectConnect += OnRemoteDirectConnect;
+
+ // Check for updates in the background
+ var webServices = modData.Manifest.Get();
+ if (Game.Settings.Debug.CheckVersion)
+ webServices.CheckModVersion();
+
+ var updateLabel = rootMenu.GetOrNull("UPDATE_NOTICE");
+ if (updateLabel != null)
+ updateLabel.IsVisible = () => !newsOpen && menuType != MenuType.None &&
+ menuType != MenuType.SystemInfoPrompt &&
+ webServices.ModVersionStatus == ModVersionStatus.Outdated;
+
+ var playerProfile = widget.GetOrNull("PLAYER_PROFILE_CONTAINER");
+ if (playerProfile != null)
+ {
+ Func minimalProfile = () => Ui.CurrentWindow() != null;
+ Game.LoadWidget(world, "LOCAL_PROFILE_PANEL", playerProfile, new WidgetArgs()
+ {
+ { "minimalProfile", minimalProfile }
+ });
+ }
+
+ // System information opt-out prompt
+ var sysInfoPrompt = widget.Get("SYSTEM_INFO_PROMPT");
+ sysInfoPrompt.IsVisible = () => menuType == MenuType.SystemInfoPrompt;
+ if (Game.Settings.Debug.SystemInformationVersionPrompt < SystemInformationVersion)
+ {
+ menuType = MenuType.SystemInfoPrompt;
+
+ var sysInfoCheckbox = sysInfoPrompt.Get("SYSINFO_CHECKBOX");
+ sysInfoCheckbox.IsChecked = () => Game.Settings.Debug.SendSystemInformation;
+ sysInfoCheckbox.OnClick = () => Game.Settings.Debug.SendSystemInformation ^= true;
+
+ var sysInfoData = sysInfoPrompt.Get("SYSINFO_DATA");
+ var template = sysInfoData.Get("DATA_TEMPLATE");
+ sysInfoData.RemoveChildren();
+
+ foreach (var info in GetSystemInformation().Values)
+ {
+ var label = template.Clone() as LabelWidget;
+ var text = info.First + ": " + info.Second;
+ label.GetText = () => text;
+ sysInfoData.AddChild(label);
+ }
+
+ sysInfoPrompt.Get("BACK_BUTTON").OnClick = () =>
+ {
+ Game.Settings.Debug.SystemInformationVersionPrompt = SystemInformationVersion;
+ Game.Settings.Save();
+ SwitchMenu(MenuType.Main);
+ LoadAndDisplayNews(webServices.GameNews, newsBG);
+ };
+ }
+ else
+ LoadAndDisplayNews(webServices.GameNews, newsBG);
+
+ Game.OnShellmapLoaded += OpenMenuBasedOnLastGame;
+ }
+
+ void LoadAndDisplayNews(string newsURL, Widget newsBG)
+ {
+ if (newsBG != null && Game.Settings.Game.FetchNews)
+ {
+ var cacheFile = Platform.ResolvePath(Platform.SupportDirPrefix, "news.yaml");
+ var currentNews = ParseNews(cacheFile);
+ if (currentNews != null)
+ DisplayNews(currentNews);
+
+ var newsButton = newsBG.GetOrNull("NEWS_BUTTON");
+ if (newsButton != null)
+ {
+ if (!fetchedNews)
+ {
+ // Send the mod and engine version to support version-filtered news (update prompts)
+ newsURL += "?version={0}&mod={1}&modversion={2}".F(
+ Uri.EscapeUriString(Game.EngineVersion),
+ Uri.EscapeUriString(Game.ModData.Manifest.Id),
+ Uri.EscapeUriString(Game.ModData.Manifest.Metadata.Version));
+
+ // Append system profile data if the player has opted in
+ if (Game.Settings.Debug.SendSystemInformation)
+ newsURL += "&sysinfoversion={0}&".F(SystemInformationVersion)
+ + GetSystemInformation()
+ .Select(kv => kv.Key + "=" + Uri.EscapeUriString(kv.Value.Second))
+ .JoinWith("&");
+
+ new Download(newsURL, cacheFile, e => { },
+ e => NewsDownloadComplete(e, cacheFile, currentNews,
+ () => OpenNewsPanel(newsButton)));
+ }
+
+ newsButton.OnClick = () => OpenNewsPanel(newsButton);
+ }
+ }
+ }
+
+ void OpenNewsPanel(DropDownButtonWidget button)
+ {
+ newsOpen = true;
+ button.AttachPanel(newsPanel, () => newsOpen = false);
+ }
+
+ void OnRemoteDirectConnect(string host, int port)
+ {
+ SwitchMenu(MenuType.None);
+ Ui.OpenWindow("MULTIPLAYER_PANEL", new WidgetArgs
+ {
+ { "onStart", RemoveShellmapUI },
+ { "onExit", () => SwitchMenu(MenuType.Main) },
+ { "directConnectHost", host },
+ { "directConnectPort", port },
+ });
+ }
+
+ void LoadMapIntoEditor(string uid)
+ {
+ ConnectionLogic.Connect(IPAddress.Loopback.ToString(),
+ Game.CreateLocalServer(uid),
+ "",
+ () => { Game.LoadEditor(uid); },
+ () => { Game.CloseServer(); SwitchMenu(MenuType.MapEditor); });
+
+ lastGameState = MenuPanel.MapEditor;
+ }
+
+ void SetNewsStatus(string message)
+ {
+ message = WidgetUtils.WrapText(message, newsStatus.Bounds.Width, Game.Renderer.Fonts[newsStatus.Font]);
+ newsStatus.GetText = () => message;
+ }
+
+ class NewsItem
+ {
+ public string Title;
+ public string Author;
+ public DateTime DateTime;
+ public string Content;
+ }
+
+ NewsItem[] ParseNews(string path)
+ {
+ if (!File.Exists(path))
+ return null;
+
+ try
+ {
+ return MiniYaml.FromFile(path).Select(node =>
+ {
+ var nodesDict = node.Value.ToDictionary();
+ return new NewsItem
+ {
+ Title = nodesDict["Title"].Value,
+ Author = nodesDict["Author"].Value,
+ DateTime = FieldLoader.GetValue("DateTime", node.Key),
+ Content = nodesDict["Content"].Value
+ };
+ }).ToArray();
+ }
+ catch (Exception ex)
+ {
+ SetNewsStatus("Failed to parse news: {0}".F(ex.Message));
+ }
+
+ return null;
+ }
+
+ void NewsDownloadComplete(AsyncCompletedEventArgs e, string cacheFile, NewsItem[] oldNews, Action onNewsDownloaded)
+ {
+ Game.RunAfterTick(() => // run on the main thread
+ {
+ if (e.Error != null)
+ {
+ SetNewsStatus("Failed to retrieve news: {0}".F(Download.FormatErrorMessage(e.Error)));
+ return;
+ }
+
+ fetchedNews = true;
+ var newNews = ParseNews(cacheFile);
+ if (newNews == null)
+ return;
+
+ DisplayNews(newNews);
+
+ if (oldNews == null || newNews.Any(n => !oldNews.Select(c => c.DateTime).Contains(n.DateTime)))
+ onNewsDownloaded();
+ });
+ }
+
+ void DisplayNews(IEnumerable newsItems)
+ {
+ newsPanel.RemoveChildren();
+ SetNewsStatus("");
+
+ foreach (var i in newsItems)
+ {
+ var item = i;
+
+ var newsItem = newsTemplate.Clone();
+
+ var titleLabel = newsItem.Get("TITLE");
+ titleLabel.GetText = () => item.Title;
+
+ var authorDateTimeLabel = newsItem.Get("AUTHOR_DATETIME");
+ var authorDateTime = authorDateTimeLabel.Text.F(item.Author, item.DateTime.ToLocalTime());
+ authorDateTimeLabel.GetText = () => authorDateTime;
+
+ var contentLabel = newsItem.Get("CONTENT");
+ var content = item.Content.Replace("\\n", "\n");
+ content = WidgetUtils.WrapText(content, contentLabel.Bounds.Width, Game.Renderer.Fonts[contentLabel.Font]);
+ contentLabel.GetText = () => content;
+ contentLabel.Bounds.Height = Game.Renderer.Fonts[contentLabel.Font].Measure(content).Y;
+ newsItem.Bounds.Height += contentLabel.Bounds.Height;
+
+ newsPanel.AddChild(newsItem);
+ newsPanel.Layout.AdjustChildren();
+ }
+ }
+
+ void RemoveShellmapUI()
+ {
+ rootMenu.Parent.RemoveChild(rootMenu);
+ }
+
+ void StartSkirmishGame()
+ {
+ var map = Game.ModData.MapCache.ChooseInitialMap(Game.Settings.Server.Map, Game.CosmeticRandom);
+ Game.Settings.Server.Map = map;
+ Game.Settings.Save();
+
+ ConnectionLogic.Connect(IPAddress.Loopback.ToString(),
+ Game.CreateLocalServer(map),
+ "",
+ OpenSkirmishLobbyPanel,
+ () => { Game.CloseServer(); SwitchMenu(MenuType.Main); });
+ }
+
+ void OpenMissionBrowserPanel()
+ {
+ SwitchMenu(MenuType.None);
+ Game.OpenWindow("MISSIONBROWSER_PANEL", new WidgetArgs
+ {
+ { "onExit", () => SwitchMenu(MenuType.Singleplayer) },
+ { "onStart", () => { RemoveShellmapUI(); lastGameState = MenuPanel.Missions; } }
+ });
+ }
+
+ void OpenSkirmishLobbyPanel()
+ {
+ SwitchMenu(MenuType.None);
+ Game.OpenWindow("SERVER_LOBBY", new WidgetArgs
+ {
+ { "onExit", () => { Game.Disconnect(); SwitchMenu(MenuType.Singleplayer); } },
+ { "onStart", () => { RemoveShellmapUI(); lastGameState = MenuPanel.Skirmish; } },
+ { "skirmishMode", true }
+ });
+ }
+
+ void OpenMultiplayerPanel()
+ {
+ SwitchMenu(MenuType.None);
+ Ui.OpenWindow("MULTIPLAYER_PANEL", new WidgetArgs
+ {
+ { "onStart", () => { RemoveShellmapUI(); lastGameState = MenuPanel.Multiplayer; } },
+ { "onExit", () => SwitchMenu(MenuType.Main) },
+ { "directConnectHost", null },
+ { "directConnectPort", 0 },
+ });
+ }
+
+ void OpenReplayBrowserPanel()
+ {
+ SwitchMenu(MenuType.None);
+ Ui.OpenWindow("REPLAYBROWSER_PANEL", new WidgetArgs
+ {
+ { "onExit", () => SwitchMenu(MenuType.Extras) },
+ { "onStart", () => { RemoveShellmapUI(); lastGameState = MenuPanel.Replays; } }
+ });
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ Game.OnRemoteDirectConnect -= OnRemoteDirectConnect;
+ Game.BeforeGameStart -= RemoveShellmapUI;
+ }
+
+ Game.OnShellmapLoaded -= OpenMenuBasedOnLastGame;
+ base.Dispose(disposing);
+ }
+
+ void OpenMenuBasedOnLastGame()
+ {
+ switch (lastGameState)
+ {
+ case MenuPanel.Missions:
+ OpenMissionBrowserPanel();
+ break;
+
+ case MenuPanel.Replays:
+ OpenReplayBrowserPanel();
+ break;
+
+ case MenuPanel.Skirmish:
+ StartSkirmishGame();
+ break;
+
+ case MenuPanel.Multiplayer:
+ OpenMultiplayerPanel();
+ break;
+
+ case MenuPanel.MapEditor:
+ SwitchMenu(MenuType.MapEditor);
+ break;
+ }
+
+ lastGameState = MenuPanel.None;
+ }
+ }
+}
diff --git a/OpenRA.Mods.YR/Widgets/Logic/VxlBrowserLogic.cs b/OpenRA.Mods.YR/Widgets/Logic/VxlBrowserLogic.cs
new file mode 100644
index 00000000..c0042cc8
--- /dev/null
+++ b/OpenRA.Mods.YR/Widgets/Logic/VxlBrowserLogic.cs
@@ -0,0 +1,471 @@
+using OpenRA.FileSystem;
+using OpenRA.Graphics;
+using OpenRA.Mods.Cnc.FileFormats;
+using OpenRA.Mods.Cnc.Graphics;
+using OpenRA.Mods.Common.Traits;
+using OpenRA.Mods.Common.Widgets;
+using OpenRA.Mods.Common.Widgets.Logic;
+using OpenRA.Widgets;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace OpenRA.Mods.YR.Widgets.Logic
+{
+ public class VxlBrowserLogic : ChromeLogic
+ {
+ readonly string[] allowedExtensions;
+ readonly IEnumerable acceptablePackages;
+
+ readonly World world;
+ readonly ModData modData;
+
+ Widget panel;
+
+ TextFieldWidget filenameInput;
+ SliderWidget frameSlider;
+ ScrollPanelWidget assetList;
+ ScrollItemWidget template;
+
+ TextFieldWidget scaleInput;
+ TextFieldWidget lightPitchInput;
+ TextFieldWidget lightYawInput;
+
+ IReadOnlyPackage assetSource = null;
+ bool animateFrames = false;
+
+ string currentPalette;
+ string currentPlayerPalette = "player";
+ string currentNormalsPalette = "normals";
+ string currentShadowPalette = "shadow";
+ int scale = 12;
+ int lightPitch = 142;
+ int lightYaw = 682;
+ float[] lightAmbientColor = new float[] {0.6f, 0.6f, 0.6f };
+ float[] lightDiffuseColor = new float[] { 0.4f, 0.4f, 0.4f };
+
+ string currentFilename;
+ IReadOnlyPackage currentPackage;
+ Voxel currentVoxel;
+ VqaPlayerWidget player = null;
+ bool isVideoLoaded = false;
+ bool isLoadError = false;
+ int currentFrame;
+
+ [ObjectCreator.UseCtor]
+ public VxlBrowserLogic(Widget widget, Action onExit, ModData modData, World world, Dictionary logicArgs)
+ {
+ this.world = world;
+ this.modData = modData;
+ panel = widget;
+
+ var ticker = panel.GetOrNull("ANIMATION_TICKER");
+ //if (ticker != null)
+ //{
+ // ticker.OnTick = () =>
+ // {
+ // if (animateFrames)
+ // SelectNextFrame();
+ // };
+ //}
+
+ var sourceDropdown = panel.GetOrNull("SOURCE_SELECTOR");
+ if (sourceDropdown != null)
+ {
+ sourceDropdown.OnMouseDown = _ => ShowSourceDropdown(sourceDropdown);
+ sourceDropdown.GetText = () =>
+ {
+ var name = assetSource != null ? Platform.UnresolvePath(assetSource.Name) : "All Packages";
+ if (name.Length > 15)
+ name = "..." + name.Substring(name.Length - 15);
+
+ return name;
+ };
+ }
+
+ var voxelWidget = panel.GetOrNull("VOXEL");
+ if (voxelWidget != null)
+ {
+ voxelWidget.GetVoxel = () => currentVoxel != null ? currentVoxel : null;
+ currentPalette = voxelWidget.Palette;
+ voxelWidget.GetPalette = () => currentPalette;
+ voxelWidget.GetPlayerPalette = () => currentPlayerPalette;
+ voxelWidget.GetNormalsPalette = () => currentNormalsPalette;
+ voxelWidget.GetShadowPalette = () => currentShadowPalette;
+ voxelWidget.GetLightAmbientColor = () => lightAmbientColor;
+ voxelWidget.GetLightDiffuseColor = () => lightDiffuseColor;
+ voxelWidget.IsVisible = () => !isVideoLoaded && !isLoadError;
+ }
+
+ var playerWidget = panel.GetOrNull("PLAYER");
+ if (playerWidget != null)
+ playerWidget.IsVisible = () => isVideoLoaded && !isLoadError;
+
+ var paletteDropDown = panel.GetOrNull("PALETTE_SELECTOR");
+ if (paletteDropDown != null)
+ {
+ paletteDropDown.OnMouseDown = _ => ShowPaletteDropdown(paletteDropDown, world);
+ paletteDropDown.GetText = () => currentPalette;
+ }
+
+ var colorPreview = panel.GetOrNull("COLOR_MANAGER");
+ if (colorPreview != null)
+ colorPreview.Color = Game.Settings.Player.Color;
+
+ var playerPaletteDropDown = panel.GetOrNull("PLAYER_PALETTE_SELECTOR");
+ if (playerPaletteDropDown != null)
+ {
+ playerPaletteDropDown.OnMouseDown = _ => ShowPlayerPaletteDropdown(playerPaletteDropDown, world);
+ playerPaletteDropDown.GetText = () => currentPlayerPalette;
+ }
+
+ var normalsPlaletteDropDown = panel.GetOrNull("NORMALS_PALETTE_SELECTOR");
+ if (normalsPlaletteDropDown != null)
+ {
+ normalsPlaletteDropDown.OnMouseDown = _ => ShowNormalsPaletteDropdown(normalsPlaletteDropDown, world);
+ normalsPlaletteDropDown.GetText = () => currentNormalsPalette;
+ }
+
+ var shadowPlaletteDropDown = panel.GetOrNull("SHADOW_PALETTE_SELECTOR");
+ if (shadowPlaletteDropDown != null)
+ {
+ shadowPlaletteDropDown.OnMouseDown = _ => ShowShadowPaletteDropdown(normalsPlaletteDropDown, world);
+ shadowPlaletteDropDown.GetText = () => currentShadowPalette;
+ }
+
+ scaleInput = panel.GetOrNull("SCALE_TEXT");
+ scaleInput.OnTextEdited = () => OnScaleEdit();
+ scaleInput.OnEscKey = scaleInput.YieldKeyboardFocus;
+
+ lightPitchInput = panel.GetOrNull("LIGHTPITCH_TEXT");
+ lightPitchInput.OnTextEdited = () => OnLightPitchEdit();
+ lightPitchInput.OnEscKey = lightPitchInput.YieldKeyboardFocus;
+
+ lightYawInput = panel.GetOrNull("LIGHTYAW_TEXT");
+ lightYawInput.OnTextEdited = () => OnLightYawEdit();
+ lightYawInput.OnEscKey = lightYawInput.YieldKeyboardFocus;
+
+
+ var lightAmbientColorDropDown = panel.GetOrNull("LIGHT_AMBIENT_COLOR");
+ if (lightAmbientColorDropDown != null)
+ {
+ //lightAmbientColorDropDown.IsDisabled = () => currentPalette != colorPreview.PaletteName;
+ lightAmbientColorDropDown.OnMouseDown = _ => ShowLightAmbientColorDropDown(lightAmbientColorDropDown, colorPreview, world);
+ panel.Get("AMBIENT_COLORBLOCK").GetColor = () => System.Drawing.Color.FromArgb(
+ Convert.ToInt32(lightAmbientColor[0] * 255),
+ Convert.ToInt32(lightAmbientColor[1] * 255),
+ Convert.ToInt32(lightAmbientColor[2] * 255)
+ );
+ }
+
+ var lightDiffuseColorDropDown = panel.GetOrNull("LIGHT_AMBIENT_COLOR");
+ if (lightDiffuseColorDropDown != null)
+ {
+ //lightDiffuseColorDropDown.IsDisabled = () => currentPalette != colorPreview.PaletteName;
+ lightDiffuseColorDropDown.OnMouseDown = _ => ShowLightDiffuseColorDropDown(lightDiffuseColorDropDown, colorPreview, world);
+ panel.Get("DIFFUSE_COLORBLOCK").GetColor = () => System.Drawing.Color.FromArgb(
+ Convert.ToInt32(lightDiffuseColor[0] * 255),
+ Convert.ToInt32(lightDiffuseColor[1] * 255),
+ Convert.ToInt32(lightDiffuseColor[2] * 255)
+ );
+ }
+
+ filenameInput = panel.Get("FILENAME_INPUT");
+ filenameInput.OnTextEdited = () => ApplyFilter();
+ filenameInput.OnEscKey = filenameInput.YieldKeyboardFocus;
+
+ if (logicArgs.ContainsKey("SupportedFormats"))
+ allowedExtensions = FieldLoader.GetValue("SupportedFormats", logicArgs["SupportedFormats"].Value);
+ else
+ allowedExtensions = new string[0];
+
+ acceptablePackages = modData.ModFiles.MountedPackages.Where(p =>
+ p.Contents.Any(c => allowedExtensions.Contains(Path.GetExtension(c).ToLowerInvariant())));
+
+ assetList = panel.Get("ASSET_LIST");
+ template = panel.Get("ASSET_TEMPLATE");
+ PopulateAssetList();
+
+ var closeButton = panel.GetOrNull("CLOSE_BUTTON");
+ if (closeButton != null)
+ closeButton.OnClick = () =>
+ {
+ if (isVideoLoaded)
+ player.Stop();
+ Ui.CloseWindow();
+ onExit();
+ };
+ }
+
+ private void OnScaleEdit()
+ {
+ string strScale = scaleInput.Text;
+ int.TryParse(strScale, out scale);
+ }
+
+ private void OnLightYawEdit()
+ {
+ string strLightYam = lightYawInput.Text;
+ int.TryParse(strLightYam, out lightYaw);
+ }
+
+ private void OnLightPitchEdit()
+ {
+ string strLightPitch = lightPitchInput.Text;
+ int.TryParse(strLightPitch, out lightPitch);
+ }
+
+ //void SelectNextFrame()
+ //{
+ // currentFrame++;
+ // if (currentFrame >= currentVoxel.Length)
+ // currentFrame = 0;
+ //}
+ //
+ //void SelectPreviousFrame()
+ //{
+ // currentFrame--;
+ // if (currentFrame < 0)
+ // currentFrame = currentVoxel.Length - 1;
+ //}
+
+ Dictionary assetVisByName = new Dictionary();
+
+ bool FilterAsset(string filename)
+ {
+ var filter = filenameInput.Text;
+
+ if (string.IsNullOrWhiteSpace(filter))
+ return true;
+
+ if (filename.IndexOf(filter, StringComparison.OrdinalIgnoreCase) >= 0)
+ return true;
+
+ return false;
+ }
+
+ void ApplyFilter()
+ {
+ assetVisByName.Clear();
+ assetList.Layout.AdjustChildren();
+ assetList.ScrollToTop();
+
+ // Select the first visible
+ var firstVisible = assetVisByName.FirstOrDefault(kvp => kvp.Value);
+ IReadOnlyPackage package;
+ string filename;
+
+ if (firstVisible.Key != null && modData.DefaultFileSystem.TryGetPackageContaining(firstVisible.Key, out package, out filename))
+ LoadAsset(package, filename);
+ }
+
+ void AddAsset(ScrollPanelWidget list, string filepath, IReadOnlyPackage package, ScrollItemWidget template)
+ {
+ var item = ScrollItemWidget.Setup(template,
+ () => currentFilename == filepath && currentPackage == package,
+ () => { LoadAsset(package, filepath); });
+
+ item.Get("TITLE").GetText = () => filepath;
+ item.IsVisible = () =>
+ {
+ bool visible;
+ if (assetVisByName.TryGetValue(filepath, out visible))
+ return visible;
+
+ visible = FilterAsset(filepath);
+ assetVisByName.Add(filepath, visible);
+ return visible;
+ };
+
+ list.AddChild(item);
+ }
+
+ bool LoadAsset(IReadOnlyPackage package, string filename)
+ {
+ if (isVideoLoaded)
+ {
+ player.Stop();
+ player = null;
+ isVideoLoaded = false;
+ }
+
+ if (string.IsNullOrEmpty(filename))
+ return false;
+
+ if (!package.Contains(filename))
+ return false;
+
+ isLoadError = false;
+
+ try
+ {
+ currentPackage = package;
+ currentFilename = filename;
+ var prefix = "";
+ var fs = modData.DefaultFileSystem as OpenRA.FileSystem.FileSystem;
+
+ if (fs != null)
+ {
+ prefix = fs.GetPrefix(package);
+ if (prefix != null)
+ prefix += "|";
+ }
+
+ VxlReader vxl;
+ HvaReader hva;
+ string filenameWithoutExtension = Path.GetFileNameWithoutExtension(filename);
+ using (var s = modData.DefaultFileSystem.Open(filenameWithoutExtension + ".vxl"))
+ vxl = new VxlReader(s);
+ using (var s = modData.DefaultFileSystem.Open(filenameWithoutExtension + ".hva"))
+ hva = new HvaReader(s, filenameWithoutExtension + ".hva");
+ VoxelLoader loader = new VoxelLoader(modData.DefaultFileSystem);
+ currentVoxel = new Voxel(loader, vxl, hva);
+ }
+ catch (Exception ex)
+ {
+ isLoadError = true;
+ Log.AddChannel("vxlbrowser", "vxlbrowser.log");
+ Log.Write("vxlbrowser", "Error reading {0}:{3} {1}{3}{2}", filename, ex.Message, ex.StackTrace, Environment.NewLine);
+
+ return false;
+ }
+
+ return true;
+ }
+
+ bool ShowSourceDropdown(DropDownButtonWidget dropdown)
+ {
+ Func setupItem = (source, itemTemplate) =>
+ {
+ var item = ScrollItemWidget.Setup(itemTemplate,
+ () => assetSource == source,
+ () => { assetSource = source; PopulateAssetList(); });
+ item.Get("LABEL").GetText = () => source != null ? Platform.UnresolvePath(source.Name) : "All Packages";
+ return item;
+ };
+
+ var sources = new[] { (IReadOnlyPackage)null }.Concat(acceptablePackages);
+ dropdown.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", 280, sources, setupItem);
+ return true;
+ }
+
+ void PopulateAssetList()
+ {
+ assetList.RemoveChildren();
+
+ var files = new SortedList>();
+
+ if (assetSource != null)
+ foreach (var content in assetSource.Contents)
+ files.Add(content, new List { assetSource });
+ else
+ {
+ foreach (var mountedPackage in modData.ModFiles.MountedPackages)
+ {
+ foreach (var content in mountedPackage.Contents)
+ {
+ if (!files.ContainsKey(content))
+ files.Add(content, new List { mountedPackage });
+ else
+ files[content].Add(mountedPackage);
+ }
+ }
+ }
+
+ foreach (var file in files.OrderBy(s => s.Key))
+ {
+ if (!allowedExtensions.Any(ext => file.Key.EndsWith(ext, true, CultureInfo.InvariantCulture)))
+ continue;
+
+ foreach (var package in file.Value)
+ AddAsset(assetList, file.Key, package, template);
+ }
+ }
+
+ bool ShowPaletteDropdown(DropDownButtonWidget dropdown, World world)
+ {
+ Func setupItem = (name, itemTemplate) =>
+ {
+ var item = ScrollItemWidget.Setup(itemTemplate,
+ () => currentPalette == name,
+ () => currentPalette = name);
+ item.Get("LABEL").GetText = () => name;
+
+ return item;
+ };
+
+ var palettes = world.WorldActor.TraitsImplementing()
+ .SelectMany(p => p.PaletteNames);
+ dropdown.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", 280, palettes, setupItem);
+ return true;
+ }
+
+ bool ShowPlayerPaletteDropdown(DropDownButtonWidget dropdown, World world)
+ {
+ Func setupItem = (name, itemTemplate) =>
+ {
+ var item = ScrollItemWidget.Setup(itemTemplate,
+ () => currentPlayerPalette == name,
+ () => currentPlayerPalette = name);
+ item.Get("LABEL").GetText = () => name;
+
+ return item;
+ };
+
+ var palettes = world.WorldActor.TraitsImplementing()
+ .SelectMany(p => p.PaletteNames);
+ dropdown.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", 280, palettes, setupItem);
+ return true;
+ }
+
+ bool ShowNormalsPaletteDropdown(DropDownButtonWidget dropdown, World world)
+ {
+ Func setupItem = (name, itemTemplate) =>
+ {
+ var item = ScrollItemWidget.Setup(itemTemplate,
+ () => currentNormalsPalette == name,
+ () => currentNormalsPalette = name);
+ item.Get("LABEL").GetText = () => name;
+
+ return item;
+ };
+
+ var palettes = world.WorldActor.TraitsImplementing()
+ .SelectMany(p => p.PaletteNames);
+ dropdown.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", 280, palettes, setupItem);
+ return true;
+ }
+
+ bool ShowShadowPaletteDropdown(DropDownButtonWidget dropdown, World world)
+ {
+ Func setupItem = (name, itemTemplate) =>
+ {
+ var item = ScrollItemWidget.Setup(itemTemplate,
+ () => currentShadowPalette == name,
+ () => currentShadowPalette = name);
+ item.Get("LABEL").GetText = () => name;
+
+ return item;
+ };
+
+ var palettes = world.WorldActor.TraitsImplementing()
+ .SelectMany(p => p.PaletteNames);
+ dropdown.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", 280, palettes, setupItem);
+ return true;
+ }
+
+ void ShowLightAmbientColorDropDown(DropDownButtonWidget color, ColorPreviewManagerWidget preview, World world)
+ {
+ ColorPickerLogic.ShowColorDropDown(color, preview, world);
+ }
+
+ void ShowLightDiffuseColorDropDown(DropDownButtonWidget color, ColorPreviewManagerWidget preview, World world)
+ {
+ ColorPickerLogic.ShowColorDropDown(color, preview, world);
+ }
+ }
+}
diff --git a/OpenRA.Mods.YR/Widgets/VoxelWidget.cs b/OpenRA.Mods.YR/Widgets/VoxelWidget.cs
new file mode 100644
index 00000000..f9cd24c2
--- /dev/null
+++ b/OpenRA.Mods.YR/Widgets/VoxelWidget.cs
@@ -0,0 +1,145 @@
+using OpenRA.Graphics;
+using OpenRA.Mods.Cnc.Graphics;
+using OpenRA.Widgets;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace OpenRA.Mods.YR.Widgets
+{
+ public class VoxelWidget : Widget
+ {
+ public string Palette = "";
+ public string PlayerPalette = "player";
+ public string NormalsPalette = "normals";
+ public string ShadowPalette = "shadow";
+ public float Scale = 12f;
+ public float[] lightAmbientColor = new float[] { 0.6f, 0.6f, 0.6f };
+ public float[] lightDiffuseColor = new float[] { 0.4f, 0.4f, 0.4f };
+ public Func GetPalette;
+ public Func GetPlayerPalette;
+ public Func GetNormalsPalette;
+ public Func GetShadowPalette;
+ public Func GetLightAmbientColor;
+ public Func GetLightDiffuseColor;
+ public Func GetScale;
+ public Func GetVoxel;
+
+ protected readonly WorldRenderer WorldRenderer;
+
+ [ObjectCreator.UseCtor]
+ public VoxelWidget(WorldRenderer worldRenderer)
+ {
+ GetPalette = () => Palette;
+ GetPlayerPalette = () => PlayerPalette;
+ GetNormalsPalette = () => NormalsPalette;
+ GetShadowPalette = () => ShadowPalette;
+ GetLightAmbientColor = () => lightAmbientColor;
+ GetLightDiffuseColor = () => lightDiffuseColor;
+ GetScale = () => Scale;
+ WorldRenderer = worldRenderer;
+ }
+
+ protected VoxelWidget(VoxelWidget other)
+ : base(other)
+ {
+ Palette = other.Palette;
+ GetPalette = other.GetPalette;
+ GetVoxel = other.GetVoxel;
+
+ WorldRenderer = other.WorldRenderer;
+ }
+
+ public override Widget Clone()
+ {
+ return new VoxelWidget(this);
+ }
+
+ private Voxel cachedVoxel;
+ private string cachedPalette;
+ private string cachedPlayerPalette;
+ private string cachedNormalsPalette;
+ private string cachedShadowPalette;
+ private float cachedScale;
+ private float[] cachedLightAmbientColor = new float[] { 0, 0, 0};
+ private float[] cachedLightDiffuseColor = new float[] { 0, 0, 0};
+ private PaletteReference pr;
+ private PaletteReference prPlayer;
+ private PaletteReference prNormals;
+ private PaletteReference prShadow;
+ private float2 offset = float2.Zero;
+ private float[] GroundNormal = new float[] { 0, 0, 1, 1 };
+
+ public override void Draw()
+ {
+ var voxel = GetVoxel();
+ var palette = GetPalette();
+ var playerPalette = GetPlayerPalette();
+ var normalsPalette = GetNormalsPalette();
+ var shadowPalette = GetShadowPalette();
+ var scale = GetScale();
+ var lightAmbientColor = GetLightAmbientColor();
+ var lightDiffuseColor = GetLightDiffuseColor();
+
+ if (voxel == null || palette == null)
+ return;
+
+ if (voxel != cachedVoxel)
+ {
+ offset = 0.5f * (new float2(RenderBounds.Size) - new float2(voxel.Size[0], voxel.Size[1]));
+ cachedVoxel = voxel;
+ }
+
+ if (palette != cachedPalette)
+ {
+ string paletteName = string.IsNullOrEmpty(palette) ? playerPalette : palette;
+ pr = WorldRenderer.Palette(paletteName);
+ cachedPalette = palette;
+ }
+
+ if (playerPalette != cachedPlayerPalette)
+ {
+ prPlayer = WorldRenderer.Palette(playerPalette);
+ cachedPlayerPalette = playerPalette;
+ }
+
+ if (normalsPalette != cachedNormalsPalette)
+ {
+ prNormals = WorldRenderer.Palette(normalsPalette);
+ cachedNormalsPalette = normalsPalette;
+ }
+
+ if (shadowPalette != cachedShadowPalette)
+ {
+ prShadow = WorldRenderer.Palette(shadowPalette);
+ cachedShadowPalette = shadowPalette;
+ }
+
+ if (scale != cachedScale)
+ {
+ offset *= scale;
+ cachedScale = scale;
+ }
+
+ if (cachedLightAmbientColor[0] != lightAmbientColor[0] || cachedLightAmbientColor[1] != lightAmbientColor[1] || cachedLightAmbientColor[2] != lightAmbientColor[2])
+ {
+ cachedLightAmbientColor = lightAmbientColor;
+ }
+
+ if (cachedLightDiffuseColor[0] != lightDiffuseColor[0] || cachedLightDiffuseColor[1] != lightDiffuseColor[1] || cachedLightDiffuseColor[2] != lightDiffuseColor[2])
+ {
+ cachedLightDiffuseColor = lightDiffuseColor;
+ }
+
+ var size = new float2(voxel.Size[0] * scale, voxel.Size[1] * scale);
+ ModelAnimation animation = new ModelAnimation(voxel, () => WVec.Zero, () => new List(){ WRot.Zero }, () => false, () => 0, true);
+
+ Game.Renderer.WorldModelRenderer.BeginFrame();
+ Game.Renderer.WorldModelRenderer.RenderAsync(WorldRenderer, new[] { animation }, new WRot(),
+ scale, GroundNormal, new WRot(), lightAmbientColor, lightDiffuseColor, pr, prNormals, prShadow);
+ Game.Renderer.WorldModelRenderer.EndFrame();
+ }
+ }
+}
diff --git a/mods/yr/chrome/mainmenu.yaml b/mods/yr/chrome/mainmenu.yaml
index b17af266..818c27af 100644
--- a/mods/yr/chrome/mainmenu.yaml
+++ b/mods/yr/chrome/mainmenu.yaml
@@ -1,5 +1,5 @@
Container@MAINMENU:
- Logic: MainMenuLogic
+ Logic: NewMainMenuLogic
Children:
LogicKeyListener@GLOBAL_KEYHANDLER:
Logic: MusicHotkeyLogic, ScreenshotHotkeyLogic, MuteHotkeyLogic
@@ -160,7 +160,7 @@ Container@MAINMENU:
Y: 180
Width: 140
Height: 30
- Text: Asset Browser
+ Text: Voxel Browser
Font: Bold
Button@CREDITS_BUTTON:
X: PARENT_RIGHT / 2 - WIDTH / 2
diff --git a/mods/yr/chrome/vxlbrowser.yaml b/mods/yr/chrome/vxlbrowser.yaml
new file mode 100644
index 00000000..d2fc0e00
--- /dev/null
+++ b/mods/yr/chrome/vxlbrowser.yaml
@@ -0,0 +1,221 @@
+Background@VXLBROWSER_PANEL:
+ Logic: VxlBrowserLogic
+ SupportedFormats: vxl
+ X: (WINDOW_RIGHT - WIDTH) / 2
+ Y: (WINDOW_BOTTOM - HEIGHT) / 2
+ Width: 800
+ Height: 500
+ Children:
+ LogicTicker@ANIMATION_TICKER:
+ ColorPreviewManager@COLOR_MANAGER:
+ Label@ASSETBROWSER_TITLE:
+ Y: 20
+ Width: PARENT_RIGHT
+ Height: 25
+ Font: Bold
+ Align: Center
+ Text: Voxel Browser
+ Label@SOURCE_SELECTOR_DESC:
+ X: 20
+ Y: 35
+ Width: 160
+ Height: 25
+ Font: TinyBold
+ Align: Center
+ Text: Select asset source
+ DropDownButton@SOURCE_SELECTOR:
+ X: 20
+ Y: 60
+ Width: 160
+ Height: 25
+ Font: Bold
+ Text: Folders
+ ScrollPanel@ASSET_LIST:
+ X: 20
+ Y: 90
+ Width: 160
+ Height: 275
+ CollapseHiddenChildren: True
+ Children:
+ ScrollItem@ASSET_TEMPLATE:
+ Width: PARENT_RIGHT - 27
+ Height: 25
+ X: 2
+ Y: 0
+ Visible: false
+ Children:
+ Label@TITLE:
+ X: 10
+ Width: PARENT_RIGHT - 20
+ Height: 25
+ Label@FILENAME_DESC:
+ X: 20
+ Y: 370
+ Width: 160
+ Height: 25
+ Font: TinyBold
+ Align: Center
+ Text: Filter by name
+ TextField@FILENAME_INPUT:
+ X: 20
+ Y: 395
+ Width: 160
+ Height: 25
+ Label@PALETTE_DESC:
+ X: 190
+ Y: 60
+ Width: 75
+ Height: 25
+ Font: Bold
+ Align: Left
+ Text: Palette:
+ DropDownButton@PALETTE_SELECTOR:
+ X: 335
+ Y: 60
+ Width: 150
+ Height: 25
+ Font: Bold
+ Label@PLAYER_PALETTE_DESC:
+ X: 190
+ Y: 94
+ Width: 75
+ Height: 25
+ Font: Bold
+ Align: Left
+ Text: Player Palette:
+ DropDownButton@PLAYER_PALETTE_SELECTOR:
+ X: 335
+ Y: 94
+ Width: 150
+ Height: 25
+ Font: Bold
+ Label@NORMALS_PALETTE_DESC:
+ X: 190
+ Y: 128
+ Width: 75
+ Height: 25
+ Font: Bold
+ Align: Left
+ Text: Normals Palette:
+ DropDownButton@NORMALS_PALETTE_SELECTOR:
+ X: 335
+ Y: 128
+ Width: 150
+ Height: 25
+ Font: Bold
+ Label@SHADOW_PALETTE_DESC:
+ X: 190
+ Y: 162
+ Width: 150
+ Height: 25
+ Font: Bold
+ Align: Left
+ Text: Shadow Palette:
+ DropDownButton@SHADOW_PALETTE_SELECTOR:
+ X: 335
+ Y: 162
+ Width: 150
+ Height: 25
+ Font: Bold
+ Label@SCALE_DESC:
+ X: 190
+ Y: 196
+ Width: 75
+ Height: 25
+ Font: Bold
+ Align: Left
+ Text: Scale:
+ TextField@SCALE_TEXT:
+ X: 335
+ Y: 196
+ Width: 150
+ Height: 25
+ Text: 12
+ Label@LIGHTPITCH_DESC:
+ X: 190
+ Y: 230
+ Width: 75
+ Height: 25
+ Font: Bold
+ Align: Left
+ Text: Light Pitch:
+ TextField@LIGHTPITCH_TEXT:
+ X: 335
+ Y: 230
+ Width: 150
+ Height: 25
+ Text: 142
+ Label@LIGHTYAW_DESC:
+ X: 190
+ Y: 264
+ Width: 150
+ Height: 25
+ Font: Bold
+ Align: Left
+ Text: Light Yaw:
+ TextField@LIGHTYAW_TEXT:
+ X: 335
+ Y: 264
+ Width: 150
+ Height: 25
+ Text: 682
+ Label@LIGHTAMBIENTCOLOR_DESC:
+ X: 190
+ Y: 298
+ Width: 150
+ Height: 25
+ Font: Bold
+ Align: Left
+ Text: Light Ambient Color:
+ DropDownButton@LIGHT_AMBIENT_COLOR:
+ X: 335
+ Y: 298
+ Width: 80
+ Height: 25
+ Children:
+ ColorBlock@AMBIENT_COLORBLOCK:
+ X: 5
+ Y: 6
+ Width: PARENT_RIGHT - 35
+ Height: PARENT_BOTTOM - 12
+ Label@LIGHTDIFFUSECOLOR_DESC:
+ X: 190
+ Y: 332
+ Width: 150
+ Height: 25
+ Font: Bold
+ Align: Left
+ Text: Light Diffuse Color:
+ DropDownButton@LIGHT_DIFFUSE_COLOR:
+ X: 335
+ Y: 332
+ Width: 80
+ Height: 25
+ Children:
+ ColorBlock@DIFFUSE_COLORBLOCK:
+ X: 5
+ Y: 6
+ Width: PARENT_RIGHT - 35
+ Height: PARENT_BOTTOM - 12
+ Background@SPRITE_BG:
+ X: PARENT_RIGHT - WIDTH - 20
+ Y: 60
+ Width: 285
+ Height: 360
+ Background: dialog3
+ Children:
+ Voxel@VOXEL:
+ Width: PARENT_RIGHT
+ Height: PARENT_BOTTOM
+ VqaPlayer@PLAYER:
+ Width: PARENT_RIGHT
+ Height: PARENT_BOTTOM
+ AspectRatio: 1
+ Button@CLOSE_BUTTON:
+ Key: escape
+ X: PARENT_RIGHT - 180
+ Y: PARENT_BOTTOM - 45
+ Width: 160
+ Height: 25
+ Font: Bold
+ Text: Close
diff --git a/mods/yr/mod.yaml b/mods/yr/mod.yaml
index e28ad707..563a8359 100644
--- a/mods/yr/mod.yaml
+++ b/mods/yr/mod.yaml
@@ -142,6 +142,7 @@ ChromeLayout:
yr|chrome/ingame-player.yaml
yr|chrome/ingame-infoobjectives.yaml
yr|chrome/mainmenu.yaml
+ yr|chrome/vxlbrowser.yaml
common|chrome/ingame-infostats.yaml
common|chrome/ingame-observerstats.yaml
common|chrome/musicplayer.yaml