diff --git a/CUE4Parse b/CUE4Parse index 566bc1f7..b5a3fd7f 160000 --- a/CUE4Parse +++ b/CUE4Parse @@ -1 +1 @@ -Subproject commit 566bc1f731b993a09b50772fef8de4d9d5e36243 +Subproject commit b5a3fd7fc4463740e885ea1d17dc9a1697b3b9b9 diff --git a/FModel/Creator/Bases/FN/BaseCommunity.cs b/FModel/Creator/Bases/FN/BaseCommunity.cs index 2492426d..39c91fa4 100644 --- a/FModel/Creator/Bases/FN/BaseCommunity.cs +++ b/FModel/Creator/Bases/FN/BaseCommunity.cs @@ -123,15 +123,9 @@ public class BaseCommunity : BaseIcon { if (!bShort) return base.GetCosmeticSeason(seasonNumber); var s = seasonNumber["Cosmetics.Filter.Season.".Length..]; - (int chapterIdx, int seasonIdx) = GetInternalSID(int.Parse(s)); - return s switch - { - "10" => $"C{chapterIdx} SX", - "27" => $"Fortnite: OG", - "32" => $"Fortnite: Remix", - "35" => $"C{chapterIdx} MS1", - _ => $"C{chapterIdx} S{seasonIdx}" - }; + (string chapterIdx, string seasonIdx, bool onlySeason) = GetInternalSID(s); + var prefix = int.TryParse(seasonIdx, out _) ? "S" : ""; + return onlySeason ? $"{prefix}{seasonIdx}" : $"C{chapterIdx} {prefix}{seasonIdx}"; } private new void DrawBackground(SKCanvas c) diff --git a/FModel/Creator/Bases/FN/BaseIcon.cs b/FModel/Creator/Bases/FN/BaseIcon.cs index e131f358..00c1e675 100644 --- a/FModel/Creator/Bases/FN/BaseIcon.cs +++ b/FModel/Creator/Bases/FN/BaseIcon.cs @@ -221,55 +221,32 @@ public class BaseIcon : UCreator return Utils.RemoveHtmlTags(string.Format(format, name)); } - protected (int, int) GetInternalSID(int number) + protected (string, string, bool) GetInternalSID(string number) { - static int GetSeasonsInChapter(int chapter) => chapter switch - { - 1 => 10, - 2 => 8, - 3 => 4, - 4 => 5, - 5 => 5, - _ => 10 - }; + if (!Utils.TryLoadObject("FortniteGame/Plugins/GameFeatures/BattlePassBase/Content/DataTables/Athena_SeasonTitles.Athena_SeasonTitles", out UDataTable seasonTitles) || + !seasonTitles.TryGetDataTableRow(number, StringComparison.InvariantCulture, out var row) || + !row.TryGetValue(out FText chapterText, "DisplayChapterText") || + !row.TryGetValue(out FText seasonText, "DisplaySeasonText") || + !row.TryGetValue(out FName displayType, "DisplayType")) + return (string.Empty, string.Empty, true); - var chapterIdx = 0; - var seasonIdx = 0; - while (number > 0) - { - var seasonsInChapter = GetSeasonsInChapter(++chapterIdx); - if (number > seasonsInChapter) - number -= seasonsInChapter; - else - { - seasonIdx = number; - number = 0; - } - } - return (chapterIdx, seasonIdx); + var onlySeason = displayType.Text.EndsWith("::OnlySeason") || (chapterText.Text == seasonText.Text && !int.TryParse(seasonText.Text, out _)); + return (chapterText.Text, seasonText.Text, onlySeason); } protected string GetCosmeticSeason(string seasonNumber) { var s = seasonNumber["Cosmetics.Filter.Season.".Length..]; - var initial = int.Parse(s); - (int chapterIdx, int seasonIdx) = GetInternalSID(initial); + (string chapterIdx, string seasonIdx, bool onlySeason) = GetInternalSID(s); var season = Utils.GetLocalizedResource("AthenaSeasonItemDefinitionInternal", "SeasonTextFormat", "Season {0}"); var introduced = Utils.GetLocalizedResource("Fort.Cosmetics", "CosmeticItemDescription_Season", "\nIntroduced in {0}."); - if (s == "10") return Utils.RemoveHtmlTags(string.Format(introduced, string.Format(season, "X"))); - if (initial <= 10) return Utils.RemoveHtmlTags(string.Format(introduced, string.Format(season, s))); + if (onlySeason) return Utils.RemoveHtmlTags(string.Format(introduced, string.Format(season, seasonIdx))); var chapter = Utils.GetLocalizedResource("AthenaSeasonItemDefinitionInternal", "ChapterTextFormat", "Chapter {0}"); var chapterFormat = Utils.GetLocalizedResource("AthenaSeasonItemDefinitionInternal", "ChapterSeasonTextFormat", "{0}, {1}"); var d = string.Format(chapterFormat, string.Format(chapter, chapterIdx), string.Format(season, seasonIdx)); - return s switch - { - "27" => Utils.RemoveHtmlTags(string.Format(introduced, string.Format("Fortnite: OG"))), - "32" => Utils.RemoveHtmlTags(string.Format(introduced, string.Format("Fortnite: Remix"))), - "35" => Utils.RemoveHtmlTags(string.Format(introduced, string.Format(chapterFormat, string.Format(chapter, chapterIdx), string.Format("MS1")))), - _ => Utils.RemoveHtmlTags(string.Format(introduced, d)) - }; + return Utils.RemoveHtmlTags(string.Format(introduced, d)); } protected void CheckGameplayTags(FInstancedStruct[] dataList) diff --git a/FModel/Creator/Bases/FN/BaseIconStats.cs b/FModel/Creator/Bases/FN/BaseIconStats.cs index c7c1e485..a297b0e0 100644 --- a/FModel/Creator/Bases/FN/BaseIconStats.cs +++ b/FModel/Creator/Bases/FN/BaseIconStats.cs @@ -95,6 +95,8 @@ public class BaseIconStats : BaseIcon weaponRowValue.TryGetValue(out float heatMax, "OverheatingMaxValue"); //Maximum heat overheating weapons can hold before they need to cool off weaponRowValue.TryGetValue(out float heatPerShot, "OverheatHeatingValue"); //Heat generated per shot on overheat weapons weaponRowValue.TryGetValue(out float overheatCooldown, "OverheatedCooldownDelay"); //Cooldown after a weapon reaches its maximum heat capacity + weaponRowValue.TryGetValue(out int cartridgePerFire, "CartridgePerFire"); //Amount of bullets shot after pressing the fire button once + weaponRowValue.TryGetValue(out float burstFiringRate, "BurstFiringRate"); //Item firing rate during a burst, value is shots per second { var multiplier = bpc != 0f ? bpc : 1; if (dmgPb != 0f) @@ -122,7 +124,12 @@ public class BaseIconStats : BaseIcon _statistics.Add(new IconStat(Utils.GetLocalizedResource("", "068239DD4327B36124498C9C5F61C038", "Magazine Size"), clipSize, 40)); } - if (firingRate != 0f) + var burstEquation = cartridgePerFire / (((cartridgePerFire - 1f) / burstFiringRate) + (1f / firingRate)); + if (burstEquation != 0f) + { + _statistics.Add(new IconStat(Utils.GetLocalizedResource("", "27B80BA44805ABD5A2D2BAB2902B250C", "Fire Rate"), burstEquation, 11)); + } + else if (firingRate != 0f) { _statistics.Add(new IconStat(Utils.GetLocalizedResource("", "27B80BA44805ABD5A2D2BAB2902B250C", "Fire Rate"), firingRate, 11)); } diff --git a/FModel/Settings/CustomDirectory.cs b/FModel/Settings/CustomDirectory.cs index 14b4a388..f368a7c4 100644 --- a/FModel/Settings/CustomDirectory.cs +++ b/FModel/Settings/CustomDirectory.cs @@ -30,6 +30,21 @@ public class CustomDirectory : ViewModel new("Shop Backgrounds", "ShooterGame/Content/UI/OutOfGame/MainMenu/Store/Shared/Textures/"), new("Weapon Renders", "ShooterGame/Content/UI/Screens/OutOfGame/MainMenu/Collection/Assets/Large/") }; + case "Dead by Daylight": + return new List + { + new("Characters V1", "DeadByDaylight/Plugins/DBDCharacters/"), + new("Characters V2", "DeadByDaylight/Plugins/Runtime/Bhvr/DBDCharacters/"), + new("Characters (Deprecated)", "DeadbyDaylight/Content/Characters/"), + new("Meshes", "DeadByDaylight/Content/Meshes/"), + new("Textures", "DeadByDaylight/Content/Textures/"), + new("Icons", "DeadByDaylight/Content/UI/UMGAssets/Icons/"), + new("Blueprints", "DeadByDaylight/Content/Blueprints/"), + new("Audio Events", "DeadByDaylight/Content/Audio/Events/"), + new("Audio", "DeadByDaylight/Content/WwiseAudio/Cooked/"), + new("Data Tables", "DeadByDaylight/Content/Data/"), + new("Localization", "DeadByDaylight/Content/Localization/") + }; default: return new List(); } diff --git a/FModel/Settings/UserSettings.cs b/FModel/Settings/UserSettings.cs index c812d4ce..ef577f0b 100644 --- a/FModel/Settings/UserSettings.cs +++ b/FModel/Settings/UserSettings.cs @@ -73,7 +73,8 @@ namespace FModel.Settings CompressionFormat = Default.CompressionFormat, Platform = Default.CurrentDir.TexturePlatform, ExportMorphTargets = Default.SaveMorphTargets, - ExportMaterials = Default.SaveEmbeddedMaterials + ExportMaterials = Default.SaveEmbeddedMaterials, + ExportHdrTexturesAsHdr = Default.SaveHdrTexturesAsHdr }; private bool _showChangelog = true; @@ -446,6 +447,13 @@ namespace FModel.Settings set => SetProperty(ref _cameraMode, value); } + private int _wwiseMaxBnkPrefetch; + public int WwiseMaxBnkPrefetch + { + get => _wwiseMaxBnkPrefetch; + set => SetProperty(ref _wwiseMaxBnkPrefetch, value); + } + private int _previewMaxTextureSize = 1024; public int PreviewMaxTextureSize { @@ -508,5 +516,12 @@ namespace FModel.Settings get => _saveSkeletonAsMesh; set => SetProperty(ref _saveSkeletonAsMesh, value); } + + private bool _saveHdrTexturesAsHdr = true; + public bool SaveHdrTexturesAsHdr + { + get => _saveHdrTexturesAsHdr; + set => SetProperty(ref _saveHdrTexturesAsHdr, value); + } } } diff --git a/FModel/ViewModels/CUE4ParseViewModel.cs b/FModel/ViewModels/CUE4ParseViewModel.cs index 2590b975..bd88b848 100644 --- a/FModel/ViewModels/CUE4ParseViewModel.cs +++ b/FModel/ViewModels/CUE4ParseViewModel.cs @@ -31,6 +31,7 @@ using CUE4Parse.UE4.Localization; using CUE4Parse.UE4.Objects.Core.Serialization; using CUE4Parse.UE4.Objects.Engine; using CUE4Parse.UE4.Oodle.Objects; +using CUE4Parse.UE4.Pak; using CUE4Parse.UE4.Readers; using CUE4Parse.UE4.Shaders; using CUE4Parse.UE4.Versions; @@ -38,7 +39,9 @@ using CUE4Parse.UE4.Wwise; using CUE4Parse_Conversion; using CUE4Parse_Conversion.Sounds; using CUE4Parse.FileProvider.Objects; +using CUE4Parse.GameTypes.AshEchoes.FileProvider; using CUE4Parse.UE4.Assets; +using CUE4Parse.UE4.BinaryConfig; using CUE4Parse.UE4.Objects.UObject; using CUE4Parse.Utils; using EpicManifestParser; @@ -86,6 +89,13 @@ public class CUE4ParseViewModel : ViewModel set => SetProperty(ref _modelIsOverwritingMaterial, value); } + private bool _modelIsWaitingAnimation; + public bool ModelIsWaitingAnimation + { + get => _modelIsWaitingAnimation; + set => SetProperty(ref _modelIsWaitingAnimation, value); + } + public bool IsSnooperOpen => _snooper is { Exists: true, IsVisible: true }; private Snooper _snooper; @@ -126,6 +136,8 @@ public class CUE4ParseViewModel : ViewModel public SearchViewModel SearchVm { get; } public TabControlViewModel TabControl { get; } public ConfigIni IoStoreOnDemand { get; } + private Lazy _wwiseProviderLazy; + public WwiseProvider WwiseProvider => _wwiseProviderLazy.Value; public CUE4ParseViewModel() { @@ -164,6 +176,7 @@ public class CUE4ParseViewModel : ViewModel [ new(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData) + "\\KONAMI\\eFootball\\ST\\Download") ], SearchOption.AllDirectories, versionContainer, pathComparer), + _ when versionContainer.Game is EGame.GAME_AshEchoes => new AEDefaultFileProvider(gameDirectory, SearchOption.AllDirectories, versionContainer, pathComparer), _ => new DefaultFileProvider(gameDirectory, SearchOption.AllDirectories, versionContainer, pathComparer) }; @@ -275,6 +288,7 @@ public class CUE4ParseViewModel : ViewModel } Provider.Initialize(); + _wwiseProviderLazy = new Lazy(() => new WwiseProvider(Provider, UserSettings.Default.WwiseMaxBnkPrefetch)); Log.Information($"{Provider.Versions.Game} ({Provider.Versions.Platform}) | Archives: x{Provider.UnloadedVfs.Count} | AES: x{Provider.RequiredKeys.Count} | Loose Files: x{Provider.Files.Count}"); }); } @@ -594,6 +608,16 @@ public class CUE4ParseViewModel : ViewModel break; } + case "ini" when entry.Name.Contains("BinaryConfig"): + { + var ar = entry.CreateReader(); + var configCache = new FConfigCacheIni(ar); + + TabControl.SelectedTab.Highlighter = AvalonExtensions.HighlighterSelector("json"); + TabControl.SelectedTab.SetDocumentText(JsonConvert.SerializeObject(configCache, Formatting.Indented), saveProperties, updateUi); + + break; + } case "upluginmanifest": case "code-workspace": case "projectstore": @@ -606,6 +630,7 @@ public class CUE4ParseViewModel : ViewModel case "gitignore": case "LICENSE": case "template": + case "stUMeta": // LIS: Double Exposure case "vmodule": case "glslfx": case "cptake": @@ -691,9 +716,11 @@ public class CUE4ParseViewModel : ViewModel var archive = entry.CreateReader(); var wwise = new WwiseReader(archive); TabControl.SelectedTab.SetDocumentText(JsonConvert.SerializeObject(wwise, Formatting.Indented), saveProperties, updateUi); - foreach (var (name, data) in wwise.WwiseEncodedMedias) + + var medias = WwiseProvider.ExtractBankSounds(wwise); + foreach (var media in medias) { - SaveAndPlaySound(entry.Path.SubstringBeforeWithLast('/') + name, "WEM", data); + SaveAndPlaySound(media.OutputPath, media.Extension, media.Data); } break; @@ -772,6 +799,8 @@ public class CUE4ParseViewModel : ViewModel break; } case "res": // just skip + case "luac": // compiled lua + case "bytes": // wuthering waves break; default: { @@ -878,22 +907,12 @@ public class CUE4ParseViewModel : ViewModel TabControl.SelectedTab.AddImage(sourceFile.SubstringAfterLast('/'), false, bitmap, false, updateUi); return false; } - case UAkAudioEvent when isNone && pointer.Object.Value is UAkAudioEvent { EventCookedData: { } wwiseData }: + case UAkAudioEvent when isNone && pointer.Object.Value is UAkAudioEvent audioEvent: { - foreach (var kvp in wwiseData.EventLanguageMap) + var extractedSounds = WwiseProvider.ExtractAudioEventSounds(audioEvent); + foreach (var sound in extractedSounds) { - if (!kvp.Value.HasValue) continue; - - foreach (var media in kvp.Value.Value.Media) - { - if (!Provider.TrySaveAsset(Path.Combine("Game/WwiseAudio/", media.MediaPathName.Text), out var data)) continue; - - var namedPath = string.Concat( - Provider.ProjectName, "/Content/WwiseAudio/", - media.DebugName.Text.SubstringBeforeLast('.').Replace('\\', '/'), - " (", kvp.Key.LanguageName.Text, ")"); - SaveAndPlaySound(namedPath, media.MediaPathName.Text.SubstringAfterLast('.'), data); - } + SaveAndPlaySound(sound.OutputPath, sound.Extension, sound.Data); } return false; } @@ -940,7 +959,7 @@ public class CUE4ParseViewModel : ViewModel SnooperViewer.Run(); return true; } - case UAnimSequenceBase when isNone && UserSettings.Default.PreviewAnimations || SnooperViewer.Renderer.Options.ModelIsWaitingAnimation: + case UAnimSequenceBase when isNone && UserSettings.Default.PreviewAnimations || ModelIsWaitingAnimation: { // animate all animations using their specified skeleton or when we explicitly asked for a loaded model to be animated (ignoring whether we wanted to preview animations) SnooperViewer.Renderer.Animate(pointer.Object.Value); diff --git a/FModel/ViewModels/GameSelectorViewModel.cs b/FModel/ViewModels/GameSelectorViewModel.cs index 870160bf..b30ebf6d 100644 --- a/FModel/ViewModels/GameSelectorViewModel.cs +++ b/FModel/ViewModels/GameSelectorViewModel.cs @@ -99,7 +99,7 @@ public class GameSelectorViewModel : ViewModel yield return GetUnrealEngineGame("9361c8c6d2f34b42b5f2f61093eedf48", "\\TslGame\\Content\\Paks", EGame.GAME_PlayerUnknownsBattlegrounds); yield return GetRiotGame("VALORANT", "ShooterGame\\Content\\Paks", EGame.GAME_Valorant); yield return DirectorySettings.Default("VALORANT [LIVE]", Constants._VAL_LIVE_TRIGGER, ue: EGame.GAME_Valorant); - yield return GetSteamGame(381210, "\\DeadByDaylight\\Content\\Paks", EGame.GAME_UE4_27); // Dead By Daylight + yield return GetSteamGame(381210, "\\DeadByDaylight\\Content\\Paks", EGame.GAME_DeadByDaylight, aesKey: "0x22b1639b548124925cf7b9cbaa09f9ac295fcf0324586d6b37ee1d42670b39b3"); // Dead By Daylight yield return GetSteamGame(578080, "\\TslGame\\Content\\Paks", EGame.GAME_PlayerUnknownsBattlegrounds); // PUBG yield return GetSteamGame(1172380, "\\SwGame\\Content\\Paks", EGame.GAME_StarWarsJediFallenOrder); // STAR WARS Jedi: Fallen Order™ yield return GetSteamGame(677620, "\\PortalWars\\Content\\Paks", EGame.GAME_Splitgate); // Splitgate @@ -151,13 +151,13 @@ public class GameSelectorViewModel : ViewModel return null; } - private DirectorySettings GetSteamGame(int id, string pakDirectory, EGame ueVersion) + private DirectorySettings GetSteamGame(int id, string pakDirectory, EGame ueVersion, string aesKey = "") { var steamInfo = SteamDetection.GetSteamGameById(id); if (steamInfo is not null) { Log.Debug("Found {GameName} in steam manifests", steamInfo.Name); - return DirectorySettings.Default(steamInfo.Name, $"{steamInfo.GameRoot}{pakDirectory}", ue: ueVersion); + return DirectorySettings.Default(steamInfo.Name, $"{steamInfo.GameRoot}{pakDirectory}", ue: ueVersion, aes: aesKey); } return null; diff --git a/FModel/ViewModels/TabControlViewModel.cs b/FModel/ViewModels/TabControlViewModel.cs index f9f9eb96..9131169a 100644 --- a/FModel/ViewModels/TabControlViewModel.cs +++ b/FModel/ViewModels/TabControlViewModel.cs @@ -105,7 +105,7 @@ public class TabImage : ViewModel if (PixelFormatUtils.IsHDR(bitmap.PixelFormat) || (UserSettings.Default.TextureExportFormat != ETextureFormat.Jpeg && UserSettings.Default.TextureExportFormat != ETextureFormat.Png)) { - ImageBuffer = bitmap.Encode(UserSettings.Default.TextureExportFormat, out var ext); + ImageBuffer = bitmap.Encode(UserSettings.Default.TextureExportFormat, UserSettings.Default.SaveHdrTexturesAsHdr, out var ext); ExportName += "." + ext; } else diff --git a/FModel/Views/Resources/Resources.xaml b/FModel/Views/Resources/Resources.xaml index c42ee085..69a239d0 100644 --- a/FModel/Views/Resources/Resources.xaml +++ b/FModel/Views/Resources/Resources.xaml @@ -1,4 +1,4 @@ - + + + @@ -322,6 +327,7 @@ + @@ -494,6 +500,11 @@ + + + diff --git a/FModel/Views/Snooper/Options.cs b/FModel/Views/Snooper/Options.cs index 0f55dd27..7b4f434b 100644 --- a/FModel/Views/Snooper/Options.cs +++ b/FModel/Views/Snooper/Options.cs @@ -16,7 +16,6 @@ namespace FModel.Views.Snooper; public class Options { public FGuid SelectedModel { get; private set; } - public bool ModelIsWaitingAnimation { get; private set; } public int SelectedSection { get; private set; } public int SelectedMorph { get; private set; } public int SelectedAnimation{ get; private set; } @@ -238,7 +237,7 @@ public class Options public void AnimateMesh(bool value) { - ModelIsWaitingAnimation = value; + Services.ApplicationService.ApplicationView.CUE4Parse.ModelIsWaitingAnimation = value; } public void ResetModelsLightsAnimations() diff --git a/FModel/Views/Snooper/Renderer.cs b/FModel/Views/Snooper/Renderer.cs index a2317032..0a3a6795 100644 --- a/FModel/Views/Snooper/Renderer.cs +++ b/FModel/Views/Snooper/Renderer.cs @@ -123,7 +123,7 @@ public class Renderer : IDisposable public void Animate(UObject anim) { - if (!Options.ModelIsWaitingAnimation) + if (!Services.ApplicationService.ApplicationView.CUE4Parse.ModelIsWaitingAnimation) { if (anim is UAnimSequenceBase animBase) { diff --git a/FModel/Views/Snooper/SnimGui.cs b/FModel/Views/Snooper/SnimGui.cs index c8cf6f40..27112d6f 100644 --- a/FModel/Views/Snooper/SnimGui.cs +++ b/FModel/Views/Snooper/SnimGui.cs @@ -624,6 +624,9 @@ Snooper aims to give an accurate preview of models, materials, skeletal animatio ImGui.EndTable(); } + ImGui.SeparatorText("Manual Inputs"); + model.Transforms[model.SelectedInstance].ImGuiTransform(s.Renderer.CameraOp.Speed / 100f); + ImGui.EndTabItem(); } diff --git a/FModel/Views/Snooper/Transform.cs b/FModel/Views/Snooper/Transform.cs index ec9d270a..5f81874f 100644 --- a/FModel/Views/Snooper/Transform.cs +++ b/FModel/Views/Snooper/Transform.cs @@ -1,5 +1,6 @@ using System.Numerics; using CUE4Parse.UE4.Objects.Core.Math; +using ImGuiNET; namespace FModel.Views.Snooper; @@ -48,5 +49,55 @@ public class Transform ModifyLocal(_saved.Value); } + public void ImGuiTransform(float speed) + { + const float width = 100f; + + if (ImGui.TreeNode("Position")) + { + ImGui.SetNextItemWidth(width); + ImGui.DragFloat("X", ref Position.X, speed, 0f, 0f, "%.2f m"); + + ImGui.SetNextItemWidth(width); + ImGui.DragFloat("Y", ref Position.Y, speed, 0f, 0f, "%.2f m"); + + ImGui.SetNextItemWidth(width); + ImGui.DragFloat("Z", ref Position.Z, speed, 0f, 0f, "%.2f m"); + + ImGui.TreePop(); + } + + if (ImGui.TreeNode("Rotation")) + { + ImGui.SetNextItemWidth(width); + ImGui.DragFloat("W", ref Rotation.W, .005f, 0f, 0f, "%.3f rad"); + + ImGui.SetNextItemWidth(width); + ImGui.DragFloat("X", ref Rotation.X, .005f, 0f, 0f, "%.3f rad"); + + ImGui.SetNextItemWidth(width); + ImGui.DragFloat("Y", ref Rotation.Y, .005f, 0f, 0f, "%.3f rad"); + + ImGui.SetNextItemWidth(width); + ImGui.DragFloat("Z", ref Rotation.Z, .005f, 0f, 0f, "%.3f rad"); + + ImGui.TreePop(); + } + + if (ImGui.TreeNode("Scale")) + { + ImGui.SetNextItemWidth(width); + ImGui.DragFloat("X", ref Scale.X, speed, 0f, 0f, "%.3f"); + + ImGui.SetNextItemWidth(width); + ImGui.DragFloat("Y", ref Scale.Y, speed, 0f, 0f, "%.3f"); + + ImGui.SetNextItemWidth(width); + ImGui.DragFloat("Z", ref Scale.Z, speed, 0f, 0f, "%.3f"); + + ImGui.TreePop(); + } + } + public override string ToString() => Matrix.Translation.ToString(); }