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