using BOTWToolset.Debugging; using BOTWToolset.IO; using BOTWToolset.IO.EXTM; using BOTWToolset.IO.SARC; using BOTWToolset.IO.TSCB; using BOTWToolset.IO.Yaz0; using Microsoft.Win32; using System; using System.IO; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; namespace BOTWToolset.Control { /// /// Interaction logic for the TSCB tab. /// public partial class TabTSCB : UserControl { /// /// The file location for the currently loaded TSCB file. /// public static string fileLocation; /// /// The currently loaded TSCB, if any. /// public static TSCB currentTSCB; /// /// WriteableBitmap that is used for the map display. /// public static WriteableBitmap writeableBitmap; /// /// Used to modify the map display with new info. /// /// The SARC archive to read for data. /// X and Y integer offsets. /// private delegate void IteratePixels(SARC sarc, int[] xyoffs, int i); public TabTSCB() { InitializeComponent(); RenderOptions.SetBitmapScalingMode(PixelView, BitmapScalingMode.NearestNeighbor); // Nearest-neighbor sampling PixelView.Source = writeableBitmap; PixelView.Stretch = Stretch.Uniform; } private void UserControlLoaded(object sender, RoutedEventArgs e) { this.Focusable = true; this.Focus(); // Update IsEnabled on MenuItem buttons } private void ClearBitmap() { PixelView.Source = null; writeableBitmap = null; GC.Collect(); // Garbage collect to free memory } private void PixelView_UpdateView(object sender, RoutedEventArgs e) { byte zoom = (byte)SliderZoomLevel.Value; // Get the parent directory of the files directory DirectoryInfo parent = Directory.GetParent(fileLocation); string main_field_dir = Path.Combine(parent.FullName, "MainField"); Button sender_button = (Button)sender; if (Directory.Exists(main_field_dir)) { switch (sender_button.Name) { case "PixelViewMATE": { string extension = ".mate.sstera"; int img_size = (int)Math.Pow(2, 8 + zoom); int grid_size = 256; BOTWConsole.LogStatus($"Setting TSCB view to texture ({img_size / grid_size}x{img_size / grid_size})..."); ClearBitmap(); writeableBitmap = new WriteableBitmap(img_size, img_size, 16, 16, PixelFormats.Indexed8, GridColors.MaterialPalette); PixelView.Source = writeableBitmap; PixelView_SetBitmap(main_field_dir, extension, zoom, (sarc, xyoffs, i) => { MATE[] mats = MATE.FromBytes(sarc.Files[i]); for (int j = 0; j < mats.Length; j++) { MATE m = mats[j]; int x = j % grid_size; int y = j / grid_size; // Draw pixel at the correct X, Y coordinate Int32Rect rect = new Int32Rect(x + (xyoffs[0] * grid_size), y + (xyoffs[1] * grid_size), 1, 1); // Use water type color + brightness adjust byte[] color = new byte[] { m.Material0 }; writeableBitmap.WritePixels(rect, color, writeableBitmap.BackBufferStride, 0); } }); BOTWConsole.LogStatus($"TSCB view set to texture ({img_size / grid_size}x{img_size / grid_size})."); } break; case "PixelViewHGHT": { string extension = ".hght.sstera"; int img_size = (int)Math.Pow(2, 8 + zoom); int grid_size = 256; BOTWConsole.LogStatus($"Setting TSCB view to heightmap ({img_size / grid_size}x{img_size / grid_size})..."); ClearBitmap(); writeableBitmap = new WriteableBitmap(img_size, img_size, 16, 16, PixelFormats.Gray8, null); PixelView.Source = writeableBitmap; PixelView_SetBitmap(main_field_dir, extension, zoom, (sarc, xyoffs, i) => { HGHT h = HGHT.FromBytes(sarc.Files[i]); for (int j = 0; j < h.Heights.Length; j++) { ushort height = h.Heights[j]; int x = j % grid_size; int y = j / grid_size; // Draw pixel at the correct X, Y coordinate Int32Rect rect = new Int32Rect(x + (xyoffs[0] * grid_size), y + (xyoffs[1] * grid_size), 1, 1); // Brightness byte brightness = (byte)(height / 65535f * 255f); // Use water type color + brightness adjust byte[] color = new byte[] { brightness }; writeableBitmap.WritePixels(rect, color, writeableBitmap.BackBufferStride, 0); } }); BOTWConsole.LogStatus($"TSCB view set to heightmap ({img_size / grid_size}x{img_size / grid_size})."); } break; case "PixelViewGrassEXTM": { string extension = ".grass.extm.sstera"; int img_size = (int)Math.Pow(2, 6 + zoom); int grid_size = 64; BOTWConsole.LogStatus($"Setting TSCB view to grass ({img_size / grid_size}x{img_size / grid_size})..."); ClearBitmap(); writeableBitmap = new WriteableBitmap(img_size, img_size, 16, 16, PixelFormats.Rgb24, null); PixelView.Source = writeableBitmap; PixelView_SetBitmap(main_field_dir, extension, zoom, (sarc, xyoffs, i) => { Grass[] grasses = Grass.FromBytes(sarc.Files[i]); for (int j = 0; j < grasses.Length; j++) { Grass g = grasses[j]; int x = j % grid_size; int y = j / grid_size; // Draw pixel at the correct X, Y coordinate Int32Rect rect = new Int32Rect(x + (xyoffs[0] * grid_size), y + (xyoffs[1] * grid_size), 1, 1); // Brightness - clamp values so colors don't become completely muted float brightness = (g.Height / 255f); // Use water type color + brightness adjust byte[] color = new byte[] { (byte)(g.R * brightness), (byte)(g.G * brightness), (byte)(g.B * brightness) }; writeableBitmap.WritePixels(rect, color, writeableBitmap.BackBufferStride, 0); } }); BOTWConsole.LogStatus($"TSCB view set to grass ({img_size / grid_size}x{img_size / grid_size})."); } break; case "PixelViewWaterEXTM": { string extension = ".water.extm.sstera"; int img_size = (int)Math.Pow(2, 6 + zoom); int grid_size = 64; BOTWConsole.LogStatus($"Setting TSCB view to water ({img_size / grid_size}x{img_size / grid_size})..."); ClearBitmap(); writeableBitmap = new WriteableBitmap(img_size, img_size, 16, 16, PixelFormats.Indexed8, GridColors.WaterPalette); PixelView.Source = writeableBitmap; PixelView_SetBitmap(main_field_dir, extension, zoom, (sarc, xyoffs, i) => { Water[] waters = Water.FromBytes(sarc.Files[i]); for (int j = 0; j < waters.Length; j++) { Water w = waters[j]; int x = j % grid_size; int y = j / grid_size; // Draw pixel at the correct X, Y coordinate Int32Rect rect = new Int32Rect(x + (xyoffs[0] * grid_size), y + (xyoffs[1] * grid_size), 1, 1); // Use water type color + brightness adjust byte[] color = new byte[] { w.MaterialIndex }; writeableBitmap.WritePixels(rect, color, writeableBitmap.BackBufferStride, 0); } }); BOTWConsole.LogStatus($"TSCB view set to water ({img_size / grid_size}x{img_size / grid_size})."); } break; } } } private void PixelView_SetBitmap(string folder, string extension, byte zoom_level, IteratePixels callback) { string[] ext_files = Directory.GetFiles(folder, $"5{zoom_level}*{extension}"); foreach (string ext_file in ext_files) { byte[] yaz0_bytes = File.ReadAllBytes(Path.Combine(folder, ext_file)); byte[] yaz0_decomp = Yaz0.Decompress(yaz0_bytes); SARC s = SARC.FromBytes(yaz0_decomp); for (int i = 0; i < s.Files.Length; i++) { // Get grid index from filename string grid_index_str = s.SFNT.FileNames[i].Replace($"5{zoom_level}", "").Replace(extension.Replace(".sstera", ""), "").Replace(folder + "\\", ""); int grid_index = int.Parse(grid_index_str, System.Globalization.NumberStyles.HexNumber); int[] xyoffs = GridConverter.ZCurveToXY(grid_index); callback(s, xyoffs, i); } } } public void SetEnabled(TSCB t) { Signature.Text = t.Signature; Version.Text = t.Version.ToString() + ".0.0.0"; FileBaseOffset.Text = t.FileBaseOffset.ToString(); WorldScale.Text = t.WorldScale.ToString(); TerrainMaxHeight.Text = t.TerrainMaxHeight.ToString(); MaterialInfoLength.Text = t.MaterialInfoLength.ToString(); AreaArrayLength.Text = t.AreaArrayLength.ToString(); TileSize.Text = t.TileSize.ToString(); foreach (var child in PixelViewTypes.Children) { if (child.GetType() == typeof(Button)) { ((Button)child).IsEnabled = true; } else if (child.GetType() == typeof(TextBox)) { ((TextBox)child).IsEnabled = true; } } } public void SetDisabled() { // Clear sidebar header info Signature.Clear(); Version.Clear(); FileBaseOffset.Clear(); WorldScale.Clear(); TerrainMaxHeight.Clear(); MaterialInfoLength.Clear(); AreaArrayLength.Clear(); TileSize.Clear(); // Clear area display stack panel TSCBAreaViewer.Children.Clear(); // Clear pixel view ClearBitmap(); // Disable controls foreach (var child in PixelViewTypes.Children) { if (child.GetType() == typeof(Button)) { ((Button)child).IsEnabled = false; } else if (child.GetType() == typeof(TextBox)) { ((TextBox)child).IsEnabled = false; } } } private void Menu_FileOpen(object sender, ExecutedRoutedEventArgs e) { BOTWConsole.Log("Clicked File -> Open button"); var openFileDialog = new OpenFileDialog { InitialDirectory = @"C:\", RestoreDirectory = true, Title = "Select TSCB file", DefaultExt = "tscb", Filter = "TSCB files (*.tscb)|*.tscb", CheckFileExists = true }; if ((bool)openFileDialog.ShowDialog()) { // This is to ensure that opening a file when one is already open resets everything in the tab SetDisabled(); BOTWConsole.Log("Opening file"); TSCB t = TSCB.FromBytes(File.ReadAllBytes(openFileDialog.FileName)); // Set the current file location to the chosen file's location fileLocation = openFileDialog.FileName; // Set the current TCSBInfo to the new TSCBInfo currentTSCB = t; // Set UI sidebar to have header info, enable controls SetEnabled(t); // Allow the file to be saved MenuFileClose.IsEnabled = true; MenuFileSave.IsEnabled = true; MenuFileSaveAs.IsEnabled = true; // Really laggy, creating 9,000+ controls isn't necessarily a fantastic idea // Maybe having a filter would help? /*oreach (var area in t.AreaInfo) { Control.TSCBAreaExpander ae = new Control.TSCBAreaExpander(); ae.AreaExpander.Header = $"({area.PositionX}, {area.PositionZ})"; ae.AreaExpander.IsExpanded = false; TSCBAreaViewer.Children.Add(ae); }*/ } } private void Menu_FileSave(object sender, ExecutedRoutedEventArgs e) { BOTWConsole.Log("Clicked File -> Save button"); File.WriteAllBytes(fileLocation, TSCB.ToBytes(currentTSCB)); BOTWConsole.LogStatus($"Saved file to {fileLocation}."); } private void Menu_FileSaveAs(object sender, ExecutedRoutedEventArgs e) { BOTWConsole.Log("Clicked File -> Save As button"); SaveFileDialog saveFileDialog = new SaveFileDialog { InitialDirectory = @"C;\", RestoreDirectory = true, Title = "Save TSCB file", DefaultExt = "tscb", Filter = "TSCB files (*.tscb)|*.tscb" }; if ((bool)saveFileDialog.ShowDialog()) { File.WriteAllBytes(saveFileDialog.FileName, TSCB.ToBytes(currentTSCB)); BOTWConsole.LogStatus($"Saved file to {saveFileDialog.FileName}."); } } private void Menu_FileClose(object sender, RoutedEventArgs e) { BOTWConsole.Log("Clicked File -> Close button"); // Set the current TSCB info to nothing currentTSCB = null; // Set the tab as disabled SetDisabled(); // Since there's no file open, don't allow saving MenuFileClose.IsEnabled = false; MenuFileSave.IsEnabled = false; MenuFileSaveAs.IsEnabled = false; } private void OverrideKeyDown(object sender, KeyEventArgs e) { if (e.Key == Key.Return) { TextBox textSender = (TextBox)sender; if (float.TryParse(textSender.Text, out float overrideValue)) { // Clamp value between 0 and 1 overrideValue = Math.Clamp(overrideValue, 0.0f, 1.0f); BOTWConsole.Log($"Overriding {textSender.Name} with value {overrideValue}"); switch (textSender.Name) { case "OverrideMinTerrainHeight": foreach (var area in currentTSCB.AreaInfo) area.MinTerrainHeight = overrideValue; break; case "OverrideMaxTerrainHeight": foreach (var area in currentTSCB.AreaInfo) area.MaxTerrainHeight = overrideValue; break; case "OverrideMinWaterHeight": foreach (var area in currentTSCB.AreaInfo) area.MinWaterHeight = overrideValue; break; case "OverrideMaxWaterHeight": foreach (var area in currentTSCB.AreaInfo) area.MaxWaterHeight = overrideValue; break; } } } } } }