using System; using System.Collections; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Reflection; using System.Windows; using System.Windows.Controls; using System.Windows.Forms; using System.Windows.Media; using System.IO; using System.ComponentModel; using System.Collections.ObjectModel; using System.Text.RegularExpressions; using System.Media; using JD.Application.View.Misc; using JD.Core; using JD.Core.Logging; using JD.Core.ProjectManaging; using JD.Interfaces.Remoting; using JD.Perforce; using JD.Remoting.Gui; using Ubisoft.Perforce.Data; using MessageBox = System.Windows.MessageBox; namespace ResourceViewer { /// /// Interaction logic for MainWindow.xaml /// public partial class MainWindow : INotifyPropertyChanged, IDisposable { #region members // specific REsourceViewier plugin member private readonly ResourceViewerPlugin _plugin = null; // engine manager object that manages the connection with the engine private readonly EngineManager _engineManager = null; // statistics to be displayed in the left overall resources information grid private readonly List _statistics = new List() { new ResourceStatistics(" Selected resources "), new ResourceStatistics(" Filtered resources "), new ResourceStatistics(" All resources ") }; public event PropertyChangedEventHandler PropertyChanged; /// /// to contain the resource info objects from the engine /// private readonly List _resourceList = new List(); /// to contain the resource info objects displayed in the grid (Applied from resource type filter) private readonly List _datagridResourceList = new List(); /// to contain the different resource type to fill the combo list of resource types private readonly List _resourceTypeList = new List(); // store the last selected resource during the refresh process private string _lastResourceTypeSelected = null; // current checkout and open for add private int _checkedOutFileCount = 0; #endregion #region properties public P4Manager P4ManagerInstance => P4Manager.Instance; public bool EngineConnected => _plugin != null && _plugin.IsConnected; private ResourceStatistics OverallStats => _statistics[2]; private ResourceStatistics FilteredStats => _statistics[1]; private ResourceStatistics SelectedStats => _statistics[0]; public object FilterWarningVisibility => string.IsNullOrWhiteSpace(_findkeyTextBox.Text.ToString(CultureInfo.InvariantCulture)) == true ? Visibility.Hidden : Visibility.Visible; private bool GameDataOnly => true; #endregion #region functions public MainWindow() { InitializeComponent(); // update window title with version number Title += $" - {Version}"; PlumeLoggerAdapter plumeLoggerAdapter = PlumeLoggerAdapter.Create(); P4Manager.Instance.Initialize(plumeLoggerAdapter); _plugin = new ResourceViewerPlugin(); _engineManager = new EngineManager(_plugin); InitialUpdate(); // set the static overall stat in ResourceInfo for computing & binding call ResourceInfo.OverallStats = OverallStats; } private string Version { get { var version = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version; return $"v{version.Major.ToString()}.{version.Minor.ToString()}"; } } private void InitialUpdate() { _filterWarningUserControl.Text = "A filter is currently active"; _checkoutWarningUserControl.Text = "You have no checked out file"; foreach (object targetName in Enum.GetValues(typeof(PlatformId))) this._targetListBox.Items.Add(targetName); if (this._targetListBox.Items.Count > 0) this._targetListBox.SelectedIndex = 0; UpdateWarningFilterVisibility(); UpdateWarningPerforceVisibility(); UpdateControlsWithConnectingState(); } private void ClearResourceList() { this._resourceList.Clear(); this._resourceTypeList.Clear(); UpdateDataGrid(); } private string GetDatagridSelectedItemName() { // keep previous selection if (this._resourceDataGrid.SelectedItem != null && this._resourceDataGrid.SelectedItem is ResourceInfo) { var selectRes = this._resourceDataGrid.SelectedItem as ResourceInfo; return selectRes.ShortName; } return null; } /// /// Apply filters on the _resourceList member, to create the datagrid resource list, source for datagrid binding /// /// private void FilterResourceList() { bool addAllResourceType = true; if (this._resourceTypeComboBox.SelectedItem != null) addAllResourceType = string.Compare(this._resourceTypeComboBox.SelectedItem.ToString(), ResourceUtils.TypeHelper.ResourceAllSuffix) == 0; var utils = new ResourceUtils(); ResourceStatistics.ResetStatistic(FilteredStats); ResourceStatistics.ResetStatistic(OverallStats); string filterKey = this._findkeyTextBox.Text; foreach (ResourceInfo ri in this._resourceList) { string resTypeName = ResourceUtils.TypeHelper.ConvertEngineTypeToUIType(ri.TypeName); if (addAllResourceType || string.Compare(this._resourceTypeComboBox.SelectedItem.ToString(), resTypeName) == 0) { bool addResToGrid = true; // filter editor and engine only data if (GameDataOnly) { var filePath = Utils.Instance.PathToP4Path(ri.PathName); if (filePath.StartsWith("world/") == false) { addResToGrid = false; continue; } } // check to filter names ? if (string.IsNullOrEmpty(filterKey) == false) { string findNameRegexPattern = @"^(.*)" + filterKey + @"(.*)\.[\w]{2,4}$"; MatchCollection matches = Utils.Instance.MatchNameWithKey(ri.ShortName, findNameRegexPattern, false); addResToGrid = matches.Count > 0; } // really add the resource to the list if (addResToGrid) { _datagridResourceList.Add(ri); ResourceStatistics.UpdateStatistic(ri, FilteredStats); } ResourceStatistics.UpdateStatistic(ri, OverallStats); } } } private void RestoreDatagridSelection(string previousSelectedItemName) { // redo selection if possible int index = 0; if (string.IsNullOrEmpty(previousSelectedItemName) == false) { // search the resource by name into observable list var search = (from res in ResourceInfoDatagridList where res.ShortName == previousSelectedItemName select res).FirstOrDefault(); if (search != null) { // get current datagrid index for this item index = this._datagridResourceList.IndexOf(search); } } if (this._resourceDataGrid.Items.Count > 0) { this._resourceDataGrid.SelectedIndex = index; } } private IEnumerable SelectTextureConfiguration() { var textureConfigurationToSelect = new List(); var selectedTargets = _targetListBox.SelectedItems.OfType(); var textureResourceList = this._resourceDataGrid.SelectedItems.OfType().Where(res => res.IsTexture == true); foreach (var resourceInfo in textureResourceList) { if (resourceInfo.TextureConfigurations == null) { string tfiFilePath = Utils.Instance.PathToP4Path(DataPath.GetAbsolutePath(resourceInfo.PathName) + Utils.TFIExtension); IEnumerable textureConfigurations = null; if (File.Exists(tfiFilePath)) { textureConfigurations = new TextureConfigFileReader(tfiFilePath).Load(); } resourceInfo.UpdateConfigurations(textureConfigurations, _targetListBox.Items.OfType()); } textureConfigurationToSelect.AddRange(resourceInfo.EnumerateConfigurations(selectedTargets)); } return textureConfigurationToSelect; } /// /// Manage single selection preview /// /// resource selected in datagrid private void ManagePreviewForSingleSelection(ResourceInfo selectedResource) { // display texture if resource is visual one string rootedFilePath = Utils.Instance.PathToP4Path(DataPath.GetAbsolutePath(selectedResource.PathName)); if (selectedResource.BitmapSource == null) { Size fileImageSize = new Size(0, 0); selectedResource.BitmapSource = Utils.Instance.GetSelectedResourceBitmapSource(selectedResource, rootedFilePath, ref fileImageSize); selectedResource.FileImageSize = fileImageSize; } // if height equals width, use fill, else use uniform if (Math.Abs(selectedResource.CustomImageSize.Width - selectedResource.CustomImageSize.Height) < float.Epsilon) { _textureImage.Stretch = Stretch.Fill; } else { _textureImage.Stretch = Stretch.Uniform; } UpdateSelectedResource(selectedResource); _textureImage.Source = selectedResource.BitmapSource; } /// /// Manage multi resource selection preview icon /// /// When called, it cannot be null or empty private void ManagePreviewForMultiSelection(IEnumerable selectedItems) { // get selected resourceInfo var resourceList = selectedItems.OfType(); // find resources that are texture // only one type resource in selected ? Yes Display type res preview No ? Display multi type selection if (resourceList.Select(x => x.TypeName).Distinct().Count() <= 1) { // take the first resource to get corresponding icon, including multi texture one (true passed to GetSelectedResourceBitmapSource) ResourceInfo selectedResource = resourceList.FirstOrDefault(); Size imageSize = new Size(0, 0); string rootedFilePath = Utils.Instance.PathToP4Path(DataPath.GetAbsolutePath(selectedResource.PathName)); _textureImage.Source = Utils.Instance.GetSelectedResourceBitmapSource(selectedResource, rootedFilePath, ref imageSize, true); } else { _textureImage.Source = Utils.GetBitmapSource(Properties.Resources.Icons_256x256_NoTexture); } _labelImageSize.Content = string.Empty; } #endregion #region handlers private void OnResourceTypeComboBoxSelectionChanged(object sender, SelectionChangedEventArgs e) { UpdateDataGrid(); } private void OnFindKeyTextChanged(object sender, TextChangedEventArgs e) { var filterKeyModerator = new Moderator(); filterKeyModerator.ElapsedTime = 2; filterKeyModerator.Done += UpdateDataGridHandler; filterKeyModerator.Trigger(); } /// /// This function is event call by blob thread. The function receive the resourceInfo list extracted from blob. /// It copies the list into a local ResourceList (_resourceList member). /// The function also build the list of type collected, and update the combo list for these types. /// /// /// public void OnUpdate(object sender, UpdateEventArgs e) { if (this.Dispatcher.Thread != System.Threading.Thread.CurrentThread) { this.Dispatcher.BeginInvoke(new ResourceViewerPlugin.UpdateEventHandler(OnUpdate), new Object[] { sender, e }); } else { // fill local list _resourceList.Clear(); _resourceList.AddRange(e.ResourceInfoList); // init combo box with All types, and select it string resTypeAll = ResourceUtils.TypeHelper.ResourceAllSuffix; // systematic add for All types _resourceTypeList.Clear(); _resourceTypeList.Add(resTypeAll); _resourceTypeList.AddRange(e.ResourceTypeList); if (this._resourceTypeComboBox.SelectedItem != null) _lastResourceTypeSelected = this._resourceTypeComboBox.SelectedItem.ToString(); else _lastResourceTypeSelected = null; System.Windows.Threading.DispatcherOperation operation = this.Dispatcher.BeginInvoke(new Action(NotifyResourceTypeListChanged), System.Windows.Threading.DispatcherPriority.Normal); operation.Completed += new EventHandler(NotifyResourceTypeComboListUpdated); // update Target display _targetLabel.Content = _plugin.Platform + " connected"; // update target combolist selection according to current connect target _targetListBox.SelectedItems.Clear(); PlatformId platform; if (!Enum.TryParse(_plugin.Platform, out platform)) { platform = PlatformId.PC; } _targetListBox.SelectedItems.Add(platform); } } public void OnDataRootUpdate(object sender, UpdateDataRootPathEventArgs e) { System.Windows.Application.Current.Dispatcher.Invoke((Action) (() => { _textBoxRootDataPath.Text = e.RootPath; })); } private void OnLoaded(object sender, RoutedEventArgs e) { // init plugin settings _plugin.UpdateHandler += OnUpdate; _plugin.UpdateDataRootHandler += OnDataRootUpdate; _plugin.ConnectedChanged += OnEngineConnectionChanged; _engineConnectionStatusUserControl.EngineManager = _engineManager; _resourcePropertyGrid.RegisterPropertyValueChangedEventHandler(OnPropertyGridPropertyValueChanged); } private void OnClosing(object sender, System.ComponentModel.CancelEventArgs e) { _resourcePropertyGrid.UnregisterPropertyValueChangedEventHandler(OnPropertyGridPropertyValueChanged); _plugin.ConnectedChanged -= OnEngineConnectionChanged; _plugin.UpdateHandler -= OnUpdate; _plugin.UpdateDataRootHandler -= OnDataRootUpdate; _engineConnectionStatusUserControl.EngineManager = null; _engineManager.Disconnect(); } private void OnEngineConnectionChanged(object sender, IPlugin e) { System.Windows.Application.Current.Dispatcher.Invoke((Action) (() => { if (_plugin.IsConnected) { _plugin.RequestInitPlugin(); _plugin.RequestDumpResources( ResourceUtils.TypeHelper.ConvertUITypeToEngineType(ResourceUtils.TypeHelper.ResourceAllSuffix)); } else { ClearResourceList(); _targetLabel.Content = string.Empty; ResourceStatistics.ResetStatistic(OverallStats); ResourceStatistics.ResetStatistic(SelectedStats); ResourceStatistics.ResetStatistic(FilteredStats); } NotifyPropertyChanged("EngineConnected"); UpdateControlsWithConnectingState(); })); } private void OnPropertyGridPropertyValueChanged(object sender, PropertyValueChangedEventArgs e) { IEnumerable selectedTextureConfiguration = _resourcePropertyGrid.SelectedObjects.OfType(); foreach (var textureConfiguration in selectedTextureConfiguration) { textureConfiguration.Modify(); } } /// /// Handler of grid selection changed. Manage both single and multiple selection. /// /// /// private void OnDataGridSelectionChanged(object sender, SelectionChangedEventArgs e) { var selectedItems = this._resourceDataGrid.SelectedItems; if (selectedItems != null && selectedItems.Count > 0) { // update statistics UpdateSelectedResourcesStatistics(selectedItems); NotifyPropertyChanged("StatisticsList"); if (selectedItems.Count > 1) { ManagePreviewForMultiSelection(selectedItems); } else { var selectedResource = (ResourceInfo)selectedItems[0]; ManagePreviewForSingleSelection(selectedResource); // play sound if resource is sound type if (selectedResource.IsSound) { string rootedFilePath = Utils.Instance.PathToP4Path(DataPath.GetAbsolutePath(selectedResource.PathName)); var simpleSound = new SoundPlayer(rootedFilePath); simpleSound.Play(); } } } UpdatePreviewVisibility(); // update property grid for this resource UpdatePropertyGridSelection(); UpdatePropertyGridVisibility(); } private void OnRefreshButtonClick(object sender, RoutedEventArgs e) { string resTypeName = ResourceUtils.TypeHelper.ConvertUITypeToEngineType(this._resourceTypeComboBox.SelectedItem.ToString()); _plugin.RequestDumpResources(resTypeName); } private void OnDataGridContextMenuOpening(object sender, ContextMenuEventArgs e) { string resTypeName = ResourceUtils.TypeHelper.ConvertUITypeToEngineType(this._resourceTypeComboBox.SelectedItem.ToString()); e.Handled = true; } private void OnDataGridMouseDoubleClick(object sender, System.Windows.Input.MouseButtonEventArgs e) { var selectedItem = this._resourceDataGrid.SelectedItem; if (selectedItem != null) { var selectedResource = selectedItem as ResourceInfo; if (selectedResource != null) { string rootedFilePath = Utils.Instance.PathToP4Path(DataPath.GetAbsolutePath(selectedResource.PathName)); Utils.Instance.OpenExplorer(rootedFilePath); } } } private bool PrecheckRevisions(IEnumerable resourceToCheckout) { if (P4ManagerInstance != null && P4ManagerInstance.Connected) { var messages = new List(); foreach (var resourceInfo in resourceToCheckout) { string tfiFilePath = DataPath.GetAbsolutePath(resourceInfo.PathName) + Utils.TFIExtension; tfiFilePath = Utils.Instance.PathToP4Path(tfiFilePath); // look if file is already on depot. if (P4ManagerInstance.PerforceWrapper.IsSubmittedInDepot(tfiFilePath)) { // look if file is at head revision on client, we dont allow to edit not at head revision file DepotFileInfo file = P4ManagerInstance.PerforceWrapper.GetFileInfo(tfiFilePath); if (file.HeadRev != file.HaveRev) { messages.Add($"{resourceInfo.PathName} is at {file.HaveRev}/{file.HeadRev} revision on client side"); } } } if (messages.Count > 0) { string message = @"File(s)" + Environment.NewLine + string.Join(Environment.NewLine, messages) + Environment.NewLine + @"You have to sync files before to save/check out"; MessageBox.Show(message); } return messages.Count == 0; } return false; } private void OnSaveButtonClick(object sender, RoutedEventArgs e) { // retrieve all textures var resourceToCheckout = new List(); IEnumerable textureResources = this._resourceList.Where(r => r != null && r.IsTexture == true); // make list with texture configurations that have been modified foreach (var textureResource in textureResources) { if (textureResource.TextureConfigurations != null) { IEnumerable configurations = textureResource.TextureConfigurations.Values; if (configurations.Any(textureConfiguration => textureConfiguration.ModifyStateChanged) == true) { resourceToCheckout.Add(textureResource); } } } if (resourceToCheckout.Count > 0) { bool canCheckout = PrecheckRevisions(resourceToCheckout); if (canCheckout == true) { foreach (var resourceInfo in resourceToCheckout) { string tfiFilePath = DataPath.GetAbsolutePath(resourceInfo.PathName) + Utils.TFIExtension; tfiFilePath = Utils.Instance.PathToP4Path(tfiFilePath); OpenForEditFile(tfiFilePath); SaveFile(tfiFilePath, resourceInfo); OpenForAdd(tfiFilePath); } UpdatePropertyGridSelection(); UpdateWarningPerforceVisibility(); } } } private void OpenForAdd(string tfiFilePath) { if (P4ManagerInstance != null && P4ManagerInstance.Connected) { if (P4ManagerInstance.PerforceWrapper.IsSubmittedInDepot(tfiFilePath) == false) { if (P4ManagerInstance.PerforceWrapper.IsPendingInDepot(tfiFilePath) == false) { P4ManagerInstance.PerforceWrapper.OpenForAdd(tfiFilePath); _checkedOutFileCount++; } } } } private void SaveFile(string tfiFilePath, ResourceInfo resourceInfo) { using (var writer = new TextureConfigFileWriter(tfiFilePath)) { writer.Save(resourceInfo.TextureConfigurations.Values); Console.WriteLine(resourceInfo.ShortName + " saved."); } } private void OpenForEditFile(string tfiFilePath) { if (P4ManagerInstance != null && P4ManagerInstance.Connected) { // look if file is already on depot. if (P4ManagerInstance.PerforceWrapper.IsSubmittedInDepot(tfiFilePath)) { P4ManagerInstance.PerforceWrapper.OpenForEdit(tfiFilePath); _checkedOutFileCount++; } else { // if file exists, is readonly, but is not submitted, delete this release just before create a new one var fileInfo = new FileInfo(tfiFilePath); if (fileInfo.Exists && fileInfo.IsReadOnly) fileInfo.Delete(); } } } private void OnTargetListSelectionChanged(object sender, SelectionChangedEventArgs e) { UpdatePropertyGridSelection(); UpdatePropertyGridVisibility(); } private void OnRecookButtonClick(object sender, RoutedEventArgs e) { var selectedItem = this._resourceDataGrid.SelectedItem; if (selectedItem != null) { var selectedResource = selectedItem as ResourceInfo; if (selectedResource != null) { Utils.Instance.CookTexture(_plugin, selectedResource); // refresh texture state UpdatePropertyGridSelection(); } } } private void OnRecookAllButtonClick(object sender, RoutedEventArgs e) { IEnumerable textureResources = this._resourceList.Where(r => r != null && r.IsTexture == true); foreach (var textureResource in textureResources) { if (textureResource.TextureConfigurations != null) { IEnumerable configurations = textureResource.TextureConfigurations.Values; if (configurations.Any(textureConfiguration => textureConfiguration.CookStateChanged) == true) { Utils.Instance.CookTexture(_plugin, textureResource); // refresh texture state UpdatePropertyGridSelection(); } } } } private void OnReleaseNotesButtonClick(object sender, RoutedEventArgs e) { } private void OnHelpButtonClick(object sender, RoutedEventArgs e) { } #endregion #region notify and update private void NotifyPropertyChanged(string name) { if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(name)); } public ObservableCollection ResourceTypeComboList => new ObservableCollection(_resourceTypeList); public ObservableCollection ResourceInfoDatagridList => new ObservableCollection(_datagridResourceList); public ObservableCollection StatisticsList => new ObservableCollection(_statistics); /// /// Called to bind the combolist on _resourceTypeList member /// private void NotifyResourceTypeListChanged() { NotifyPropertyChanged("ResourceTypeComboList"); } /// /// Called to finish resource Type combo update selected one res type /// /// /// private void NotifyResourceTypeComboListUpdated(object sender, EventArgs e) { int index = 0; if (string.IsNullOrEmpty(this._lastResourceTypeSelected) == false) { if (_resourceTypeList.Contains(_lastResourceTypeSelected)) { index = _resourceTypeList.IndexOf(_lastResourceTypeSelected); } } if (this._resourceTypeComboBox.Items.Count > 0) { // force redraw on datagrid even if selection resource type did not changed if (this._resourceTypeComboBox.SelectedIndex == index) UpdateDataGrid(); this._resourceTypeComboBox.SelectedIndex = index; } } private void UpdateWarningPerforceVisibility() { if (P4ManagerInstance != null && _checkedOutFileCount > 0) { _checkoutWarningUserControl.Text = $"You checked out {_checkedOutFileCount} files in default since you began"; _checkoutWarningUserControl.Visibility = Visibility.Visible; } else { _checkoutWarningUserControl.Visibility = Visibility.Hidden; } } private void UpdateWarningFilterVisibility() { if (string.IsNullOrWhiteSpace(_findkeyTextBox.Text) == false) { _filterWarningUserControl.Visibility = Visibility.Visible; } else { _filterWarningUserControl.Visibility = Visibility.Hidden; } } /// /// Update data grid, recompute content + bind + update selection /// private void UpdateDataGrid() { string selectedItemName = GetDatagridSelectedItemName(); // clear data list _datagridResourceList.Clear(); // reset previous selection this._resourceDataGrid.SelectedItem = null; this._resourceDataGrid.SelectedIndex = -1; // compute _datagridlist according to filter FilterResourceList(); // notify control source data changed NotifyPropertyChanged("ResourceInfoDatagridList"); RestoreDatagridSelection(selectedItemName); UpdateWarningFilterVisibility(); } private void UpdateDataGridHandler(object sender, EventArgs e) { System.Windows.Threading.DispatcherOperation operation = this.Dispatcher.BeginInvoke(new Action(UpdateDataGrid), System.Windows.Threading.DispatcherPriority.Normal); } /// /// Update preview texture visibility /// private void UpdatePreviewVisibility() { System.Windows.Application.Current.Dispatcher.Invoke((Action)(() => { var visibility = Visibility.Hidden; var selectedResources = this._resourceDataGrid.SelectedItems; if (selectedResources != null && selectedResources.Count > 0) visibility = Visibility.Visible; this._labelImageSize.Visibility = visibility; this._gridimage.Visibility = visibility; })); } /// /// Update engine specific controls /// private void UpdateControlsWithConnectingState() { UpdatePreviewVisibility(); var visibility = Visibility.Hidden; if (EngineConnected) visibility = Visibility.Visible; _resourceInfoGrid.Visibility = visibility; _targetLabel.Visibility = visibility; } /// /// Display property grid only if all selected resource are texture resources /// private void UpdatePropertyGridSelection() { var selectedResources = this._resourceDataGrid.SelectedItems; if (selectedResources != null) { IEnumerable configurations = SelectTextureConfiguration(); if (configurations.Any()) { _resourcePropertyGrid.SelectedObjects = configurations.ToArray(); } } else { _resourcePropertyGrid.SelectedObjects = null; } } /// /// Display property grid only if all selected resource are texture resources /// private void UpdatePropertyGridVisibility() { var selectedResources = this._resourceDataGrid.SelectedItems.OfType(); // only one type in selection ? if (selectedResources.Select(x => x.TypeName).Distinct().Count() == 1 && selectedResources.Any(x => x.IsTexture == true)) { this._resourceGroupBox.Visibility = Visibility.Visible; } else { this._resourceGroupBox.Visibility = Visibility.Hidden; } } private void UpdateSelectedResource(ResourceInfo selectedResource) { if (selectedResource.IsTexture) { _labelImageSize.Content = $"Original image size : {selectedResource.FileImageSize.Width}x{selectedResource.FileImageSize.Height}"; } else { _labelImageSize.Content = string.Empty; } } private void UpdateSelectedResourcesStatistics(IEnumerable selectedItems) { ResourceStatistics.ResetStatistic(SelectedStats); foreach (var selectedItem in selectedItems) { var selectedResource = selectedItem as ResourceInfo; if (selectedResource != null) { ResourceStatistics.UpdateStatistic(selectedResource, SelectedStats); } } } #endregion public void Dispose() { _plugin?.Dispose(); _engineManager?.Dispose(); } } }