diff --git a/NResLib/NResArchive.cs b/NResLib/NResArchive.cs new file mode 100644 index 0000000..92202ab --- /dev/null +++ b/NResLib/NResArchive.cs @@ -0,0 +1,45 @@ +namespace NResLib; + +/// +/// Архив NRes (файл NRes) +/// +public record NResArchive(NResArchiveHeader Header, List Files); + +/// +/// Заголовок файла +/// +/// [0..4] ASCII NRes +/// [4..8] Версия кодировщика (должно быть всегда 0x100) +/// [8..12] Количество файлов +/// [12..16] Длина всего архива +public record NResArchiveHeader(string NRes, int Version, int FileCount, int TotalFileLengthBytes); + +/// +/// В конце файла есть список метаданных, +/// каждый элемент это 64 байта, +/// найти начало можно как (Header.TotalFileLengthBytes - Header.FileCount * 64) +/// +/// [0..8] ASCII описание типа файла, например TEXM или MAT0 +/// [8..12] Неизвестное число +/// [12..16] Длина файла в байтах +/// [16..20] Неизвестное число +/// [20..40] ASCII имя файла +/// [40..44] Неизвестное число +/// [44..48] Неизвестное число +/// [48..52] Неизвестное число +/// [52..56] Неизвестное число +/// [56..60] Смещение подфайла от начала NRes (именно самого NRes) в байтах +/// [60..64] Индекс в файле (от 0, не больше чем кол-во файлов) +public record ListMetadataItem( + string FileType, + int Magic1, + int FileLength, + int Magic2, + string FileName, + int Magic3, + int Magic4, + int Magic5, + int Magic6, + int OffsetInFile, + int Index +); \ No newline at end of file diff --git a/NResLib/NResLib.csproj b/NResLib/NResLib.csproj new file mode 100644 index 0000000..3a63532 --- /dev/null +++ b/NResLib/NResLib.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/NResLib/NResParseResult.cs b/NResLib/NResParseResult.cs new file mode 100644 index 0000000..1f2b671 --- /dev/null +++ b/NResLib/NResParseResult.cs @@ -0,0 +1,3 @@ +namespace NResLib; + +public record NResParseResult(NResArchive? Archive = null, string? Error = null); \ No newline at end of file diff --git a/NResLib/NResParser.cs b/NResLib/NResParser.cs new file mode 100644 index 0000000..7054565 --- /dev/null +++ b/NResLib/NResParser.cs @@ -0,0 +1,78 @@ +using System.Buffers.Binary; +using System.Text; + +namespace NResLib; + +public static class NResParser +{ + public static NResParseResult ReadFile(string path) + { + using FileStream nResFs = new FileStream(path, FileMode.Open); + + if (nResFs.Length < 16) + { + return new NResParseResult(null, "Файл не может быть NRes, менее 16 байт"); + } + + Span buffer = stackalloc byte[16]; + + nResFs.ReadExactly(buffer); + + if (buffer[0] != 'N' || buffer[1] != 'R' || buffer[2] != 'e' || buffer[3] != 's') + { + return new NResParseResult(null, "Файл не начинается с NRes"); + } + + var header = new NResArchiveHeader( + NRes: Encoding.ASCII.GetString(buffer[0..4]), + Version: BinaryPrimitives.ReadInt32LittleEndian(buffer[4..8]), + FileCount: BinaryPrimitives.ReadInt32LittleEndian(buffer[8..12]), + TotalFileLengthBytes: BinaryPrimitives.ReadInt32LittleEndian(buffer[12..16]) + ); + + if (header.TotalFileLengthBytes != nResFs.Length) + { + return new NResParseResult( + null, + $"Длина файла не совпадает с заявленным в заголовке.\n" + + $"Заявлено: {header.TotalFileLengthBytes}\n" + + $"Фактически: {nResFs.Length}" + ); + } + + nResFs.Seek(-header.FileCount * 64, SeekOrigin.End); + + var elements = new List(header.FileCount); + + Span metaDataBuffer = stackalloc byte[64]; + for (int i = 0; i < header.FileCount; i++) + { + nResFs.ReadExactly(metaDataBuffer); + + elements.Add( + new ListMetadataItem( + FileType: Encoding.ASCII.GetString(metaDataBuffer[..8]), + Magic1: BinaryPrimitives.ReadInt32LittleEndian(metaDataBuffer[8..12]), + FileLength: BinaryPrimitives.ReadInt32LittleEndian(metaDataBuffer[12..16]), + Magic2: BinaryPrimitives.ReadInt32LittleEndian(metaDataBuffer[16..20]), + FileName: Encoding.ASCII.GetString(metaDataBuffer[20..40]), + Magic3: BinaryPrimitives.ReadInt32LittleEndian(metaDataBuffer[40..44]), + Magic4: BinaryPrimitives.ReadInt32LittleEndian(metaDataBuffer[44..48]), + Magic5: BinaryPrimitives.ReadInt32LittleEndian(metaDataBuffer[48..52]), + Magic6: BinaryPrimitives.ReadInt32LittleEndian(metaDataBuffer[52..56]), + OffsetInFile: BinaryPrimitives.ReadInt32LittleEndian(metaDataBuffer[56..60]), + Index: BinaryPrimitives.ReadInt32LittleEndian(metaDataBuffer[60..64]) + ) + ); + + metaDataBuffer.Clear(); + } + + return new NResParseResult( + new NResArchive( + Header: header, + Files: elements + ) + ); + } +} \ No newline at end of file diff --git a/NResUI/Abstractions/IExitReceiver.cs b/NResUI/Abstractions/IExitReceiver.cs new file mode 100644 index 0000000..2c3d093 --- /dev/null +++ b/NResUI/Abstractions/IExitReceiver.cs @@ -0,0 +1,7 @@ +namespace NResUI.Abstractions +{ + public interface IExitReceiver + { + void OnExit(); + } +} \ No newline at end of file diff --git a/NResUI/Abstractions/IImGuiPanel.cs b/NResUI/Abstractions/IImGuiPanel.cs new file mode 100644 index 0000000..94390b2 --- /dev/null +++ b/NResUI/Abstractions/IImGuiPanel.cs @@ -0,0 +1,7 @@ +namespace NResUI.Abstractions +{ + public interface IImGuiPanel + { + void OnImGuiRender(); + } +} \ No newline at end of file diff --git a/NResUI/Abstractions/IKeyPressReceiver.cs b/NResUI/Abstractions/IKeyPressReceiver.cs new file mode 100644 index 0000000..23aa7d8 --- /dev/null +++ b/NResUI/Abstractions/IKeyPressReceiver.cs @@ -0,0 +1,17 @@ +using Silk.NET.Input; + +namespace NResUI.Abstractions +{ + public interface IKeyPressReceiver + { + void OnKeyPressed(Key key); + } + public interface IKeyReleaseReceiver + { + void OnKeyReleased(Key key); + } + public interface IKeyDownReceiver + { + void OnKeyDown(Key key); + } +} \ No newline at end of file diff --git a/NResUI/Abstractions/IService.cs b/NResUI/Abstractions/IService.cs new file mode 100644 index 0000000..f668d31 --- /dev/null +++ b/NResUI/Abstractions/IService.cs @@ -0,0 +1,6 @@ +namespace NResUI.Abstractions +{ + public interface IService + { + } +} \ No newline at end of file diff --git a/NResUI/Abstractions/IUpdateReceiver.cs b/NResUI/Abstractions/IUpdateReceiver.cs new file mode 100644 index 0000000..d338a52 --- /dev/null +++ b/NResUI/Abstractions/IUpdateReceiver.cs @@ -0,0 +1,10 @@ +namespace NResUI.Abstractions +{ + public interface IUpdateReceiver + { + /// + /// Called before every UI render + /// + void OnUpdate(float delta); + } +} \ No newline at end of file diff --git a/NResUI/App.cs b/NResUI/App.cs new file mode 100644 index 0000000..e5b6516 --- /dev/null +++ b/NResUI/App.cs @@ -0,0 +1,140 @@ +using System.Numerics; +using ImGuiNET; +using Microsoft.Extensions.DependencyInjection; +using NResUI.Abstractions; +using NResUI.Models; +using Silk.NET.Input; +using Silk.NET.OpenGL; +using Silk.NET.Windowing; + +namespace NResUI; + +public class App + { + public GL GL { get; set; } + public IInputContext Input { get; set; } + + public static App Instance; + + private static bool _dockspaceOpen = true; + private static bool _optFullscreenPersistant = true; + private static bool _optFullscreen = _optFullscreenPersistant; + + private static ImGuiDockNodeFlags _dockspaceFlags = ImGuiDockNodeFlags.None; + + public ImFontPtr OpenSansFont; + + private List _imGuiPanels; + + public App() + { + Instance = this; + } + + public void Init(IWindow window, GL openGl, ImFontPtr openSansFont) + { + ImGui.StyleColorsLight(); + + IServiceCollection serviceCollection = new ServiceCollection(); + + foreach (var type in Utils.GetAssignableTypes()) + { + serviceCollection.AddSingleton(type); + } + + foreach (var type in Utils.GetAssignableTypes()) + { + serviceCollection.AddSingleton(type); + } + + serviceCollection.AddSingleton(new ExplorerViewModel()); + + var serviceProvider = serviceCollection.BuildServiceProvider(); + + _imGuiPanels = Utils.GetAssignableTypes() + .Select(t => (serviceProvider.GetService(t) as IImGuiPanel)!) + .ToList(); + } + + public void OnImGuiRender() + { + ImGui.PushFont(OpenSansFont); + + // We are using the ImGuiWindowFlags_NoDocking flag to make the parent window not dockable into, + // because it would be confusing to have two docking targets within each others. + var windowFlags = ImGuiWindowFlags.MenuBar | ImGuiWindowFlags.NoDocking; + if (_optFullscreen) + { + var viewport = ImGui.GetMainViewport(); + ImGui.SetNextWindowPos(viewport.Pos); + ImGui.SetNextWindowSize(viewport.Size); + ImGui.SetNextWindowViewport(viewport.ID); + ImGui.PushStyleVar(ImGuiStyleVar.WindowRounding, 0.0f); + ImGui.PushStyleVar(ImGuiStyleVar.WindowBorderSize, 0.0f); + windowFlags |= ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoResize | + ImGuiWindowFlags.NoMove; + windowFlags |= ImGuiWindowFlags.NoBringToFrontOnFocus | ImGuiWindowFlags.NoNavFocus; + } + + // When using ImGuiDockNodeFlags_PassthruCentralNode, DockSpace() will render our background and handle the pass-thru hole, so we ask Begin() to not render a background. + if ((_dockspaceFlags & ImGuiDockNodeFlags.PassthruCentralNode) != 0) + windowFlags |= ImGuiWindowFlags.NoBackground; + + // Important: note that we proceed even if Begin() returns false (aka window is collapsed). + // This is because we want to keep our DockSpace() active. If a DockSpace() is inactive, + // all active windows docked into it will lose their parent and become undocked. + // We cannot preserve the docking relationship between an active window and an inactive docking, otherwise + // any change of dockspace/settings would lead to windows being stuck in limbo and never being visible. + ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, new Vector2(0.0f, 0.0f)); + ImGui.Begin("DockSpace Demo", ref _dockspaceOpen, windowFlags); + ImGui.PopStyleVar(); + + if (_optFullscreen) + ImGui.PopStyleVar(2); + + // DockSpace + var io = ImGui.GetIO(); + var style = ImGui.GetStyle(); + var minWinSizeX = style.WindowMinSize.X; + style.WindowMinSize.X = 370.0f; + if ((io.ConfigFlags & ImGuiConfigFlags.DockingEnable) != 0) + { + var dockspaceId = ImGui.GetID("MyDockSpace"); + ImGui.DockSpace(dockspaceId, new Vector2(0.0f, 0.0f), _dockspaceFlags); + } + + style.WindowMinSize.X = minWinSizeX; + + foreach (var imGuiPanel in _imGuiPanels) + { + imGuiPanel.OnImGuiRender(); + } + + ImGui.ShowMetricsWindow(); + ImGui.ShowDemoWindow(); + + ImGui.PopFont(); + + ImGui.End(); + } + + public void Exit() + { + } + + public void Update(double delta) + { + } + + public void OnKeyPressed(Key key) + { + } + + public void OnKeyDown(Key key) + { + } + + public void OnKeyReleased(Key key) + { + } + } \ No newline at end of file diff --git a/NResUI/ImGuiUI/ExplorerPanel.cs b/NResUI/ImGuiUI/ExplorerPanel.cs new file mode 100644 index 0000000..82b4189 --- /dev/null +++ b/NResUI/ImGuiUI/ExplorerPanel.cs @@ -0,0 +1,127 @@ +using ImGuiNET; +using NResUI.Abstractions; +using NResUI.Models; + +namespace NResUI.ImGuiUI; + +public class ExplorerPanel : IImGuiPanel +{ + private readonly ExplorerViewModel _viewModel; + + public ExplorerPanel(ExplorerViewModel viewModel) + { + _viewModel = viewModel; + } + + public void OnImGuiRender() + { + if (ImGui.Begin("Explorer")) + { + if (!_viewModel.HasFile) + { + ImGui.Text("No NRes is opened"); + } + else + { + if (_viewModel.Error != null) + { + ImGui.Text(_viewModel.Error); + } + + if (_viewModel.Archive is not null) + { + ImGui.Text(_viewModel.Path); + + ImGui.Text("Header: "); + ImGui.SameLine(); + ImGui.Text(_viewModel.Archive.Header.NRes); + ImGui.Text("Version: "); + ImGui.SameLine(); + ImGui.Text(_viewModel.Archive.Header.Version.ToString()); + ImGui.Text("File Count: "); + ImGui.SameLine(); + ImGui.Text(_viewModel.Archive.Header.FileCount.ToString()); + ImGui.Text("Total File Length: "); + ImGui.SameLine(); + ImGui.Text(_viewModel.Archive.Header.TotalFileLengthBytes.ToString()); + + + if (ImGui.BeginTable("content", 11)) + { + ImGui.TableSetupColumn("Тип файла"); + ImGui.TableSetupColumn("Magic1"); + ImGui.TableSetupColumn("Длина файла в байтах"); + ImGui.TableSetupColumn("Magic2"); + ImGui.TableSetupColumn("Имя файла"); + ImGui.TableSetupColumn("Magic3"); + ImGui.TableSetupColumn("Magic4"); + ImGui.TableSetupColumn("Magic5"); + ImGui.TableSetupColumn("Magic6"); + ImGui.TableSetupColumn("Смещение в байтах"); + ImGui.TableSetupColumn("Индекс в файле"); + + ImGui.TableHeadersRow(); + + for (int i = 0; i < _viewModel.Archive.Files.Count; i++) + { + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + ImGui.Text(_viewModel.Archive.Files[i].FileType); + ImGui.TableNextColumn(); + ImGui.Text( + _viewModel.Archive.Files[i] + .Magic1.ToString() + ); + ImGui.TableNextColumn(); + ImGui.Text( + _viewModel.Archive.Files[i] + .FileLength.ToString() + ); + ImGui.TableNextColumn(); + ImGui.Text( + _viewModel.Archive.Files[i] + .Magic2.ToString() + ); + ImGui.TableNextColumn(); + ImGui.Text(_viewModel.Archive.Files[i].FileName); + ImGui.TableNextColumn(); + ImGui.Text( + _viewModel.Archive.Files[i] + .Magic3.ToString() + ); + ImGui.TableNextColumn(); + ImGui.Text( + _viewModel.Archive.Files[i] + .Magic4.ToString() + ); + ImGui.TableNextColumn(); + ImGui.Text( + _viewModel.Archive.Files[i] + .Magic5.ToString() + ); + ImGui.TableNextColumn(); + ImGui.Text( + _viewModel.Archive.Files[i] + .Magic6.ToString() + ); + ImGui.TableNextColumn(); + ImGui.Text( + _viewModel.Archive.Files[i] + .OffsetInFile.ToString() + ); + ImGui.TableNextColumn(); + ImGui.Text( + _viewModel.Archive.Files[i] + .Index.ToString() + ); + } + + ImGui.EndTable(); + } + } + } + + ImGui.End(); + } + } +} \ No newline at end of file diff --git a/NResUI/ImGuiUI/ImGuiModalPanel.cs b/NResUI/ImGuiUI/ImGuiModalPanel.cs new file mode 100644 index 0000000..a772b04 --- /dev/null +++ b/NResUI/ImGuiUI/ImGuiModalPanel.cs @@ -0,0 +1,39 @@ +using System.Numerics; +using ImGuiNET; +using NResUI.Abstractions; + +namespace NResUI.ImGuiUI; + +public abstract class ImGuiModalPanel : IImGuiPanel +{ + protected abstract string ImGuiId { get; } + + private bool _shouldOpen = false; + + public virtual void Open() + { + _shouldOpen = true; + } + + protected abstract void OnImGuiRenderContent(); + + public void OnImGuiRender() + { + // this is a ImGui stack fix. Because menubars and some other controls use their separate stack context, + // The panel gets rendered on it's own, at the root of the stack, and with _shouldOpen we control, if the panel should open this frame. + if (_shouldOpen) + { + ImGui.OpenPopup(ImGuiId, ImGuiPopupFlags.AnyPopupLevel); + _shouldOpen = false; + } + + ImGui.SetNextWindowSize(new Vector2(600, 400)); + + if (ImGui.BeginPopup(ImGuiId, ImGuiWindowFlags.NoResize)) + { + OnImGuiRenderContent(); + + ImGui.EndPopup(); + } + } +} \ No newline at end of file diff --git a/NResUI/ImGuiUI/MainMenuBar.cs b/NResUI/ImGuiUI/MainMenuBar.cs new file mode 100644 index 0000000..462a9e2 --- /dev/null +++ b/NResUI/ImGuiUI/MainMenuBar.cs @@ -0,0 +1,108 @@ +using System.Numerics; +using ImGuiNET; +using NativeFileDialogSharp; +using NResLib; +using NResUI.Abstractions; +using NResUI.Models; + +namespace NResUI.ImGuiUI +{ + public class MainMenuBar : IImGuiPanel + { + private readonly ExplorerViewModel _explorerViewModel; + + public MainMenuBar(ExplorerViewModel explorerViewModel) + { + _explorerViewModel = explorerViewModel; + } + + public void OnImGuiRender() + { + if (ImGui.BeginMenuBar()) + { + if (ImGui.BeginMenu("File")) + { + if (ImGui.MenuItem("Open NRes")) + { + var result = Dialog.FileOpen(); + + if (result.IsOk) + { + var path = result.Path; + + var parseResult = NResParser.ReadFile(path); + + _explorerViewModel.SetParseResult(parseResult, path); + } + } + + if (_explorerViewModel.HasFile) + { + if (ImGui.MenuItem("Экспортировать")) + { + var result = Dialog.FolderPicker(); + + if (result.IsOk) + { + var path = result.Path; + + Console.WriteLine(path); + } + } + } + + if (ImGui.BeginMenu("Open Recent")) + { + ImGui.EndMenu(); + } + + if (ImGui.MenuItem("Exit")) + { + App.Instance.Exit(); + } + + ImGui.EndMenu(); + } + + if (ImGui.BeginMenu("Windows")) + { + if (ImGui.MenuItem("Settings")) + { + } + + ImGui.EndMenu(); + } + + ImGui.EndMenuBar(); + } + } + + // This is a direct port of imgui_demo.cpp HelpMarker function + + // https://github.com/ocornut/imgui/blob/master/imgui_demo.cpp#L190 + + private void ShowHint(string message) + { + // ImGui.TextDisabled("(?)"); + if (ImGui.IsItemHovered()) + { + // Change background transparency + ImGui.PushStyleColor( + ImGuiCol.PopupBg, + new Vector4( + 1, + 1, + 1, + 0.8f + ) + ); + ImGui.BeginTooltip(); + ImGui.PushTextWrapPos(ImGui.GetFontSize() * 35.0f); + ImGui.TextUnformatted(message); + ImGui.PopTextWrapPos(); + ImGui.EndTooltip(); + ImGui.PopStyleColor(); + } + } + } +} \ No newline at end of file diff --git a/NResUI/Models/ExplorerViewModel.cs b/NResUI/Models/ExplorerViewModel.cs new file mode 100644 index 0000000..bb83211 --- /dev/null +++ b/NResUI/Models/ExplorerViewModel.cs @@ -0,0 +1,26 @@ +using NResLib; + +namespace NResUI.Models; + +public class ExplorerViewModel +{ + public bool HasFile { get; set; } + public string? Error { get; set; } + + public NResArchive? Archive { get; set; } + + public string? Path { get; set; } + + public void SetParseResult(NResParseResult result, string path) + { + Error = result.Error; + + if (result.Archive != null) + { + HasFile = true; + } + + Archive = result.Archive; + Path = path; + } +} \ No newline at end of file diff --git a/NResUI/NResUI.csproj b/NResUI/NResUI.csproj new file mode 100644 index 0000000..16483bf --- /dev/null +++ b/NResUI/NResUI.csproj @@ -0,0 +1,25 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + + + + + + + + diff --git a/NResUI/Program.cs b/NResUI/Program.cs new file mode 100644 index 0000000..55d1c9f --- /dev/null +++ b/NResUI/Program.cs @@ -0,0 +1,109 @@ +// See https://aka.ms/new-console-template for more information + +using System.Drawing; +using ImGuiNET; +using NResUI; +using Silk.NET.Input; +using Silk.NET.OpenGL; +using Silk.NET.OpenGL.Extensions.ImGui; +using Silk.NET.Windowing; + +var window = Window.Create(WindowOptions.Default); + +// Declare some variables +ImGuiController controller = null!; +GL gl = null!; +IInputContext inputContext = null!; + +var app = new App(); + +// Our loading function +window.Load += () => +{ + var openGl = window.CreateOpenGL(); + + ImFontPtr mainFont = null; + + controller = new ImGuiController( + gl = openGl, // load OpenGL + window, // pass in our window + inputContext = window.CreateInput(), // create an input context + () => + { + var io = ImGui.GetIO(); + io.ConfigFlags |= ImGuiConfigFlags.DockingEnable; + mainFont = io.Fonts.AddFontFromFileTTF( + filename: "assets/Font/OpenSans-Regular.ttf", + size_pixels: 18, + font_cfg: null, + glyph_ranges: io.Fonts.GetGlyphRangesCyrillic() + ); + } + ); + app.Init(window, openGl, mainFont); + + inputContext.Keyboards[0] + .KeyDown += (keyboard, key, scancode) => { app.OnKeyDown(key); }; + inputContext.Keyboards[0] + .KeyUp += (keyboard, key, scancode) => { app.OnKeyPressed(key); }; + inputContext.Keyboards[0] + .KeyUp += (keyboard, key, scancode) => { app.OnKeyReleased(key); }; +}; + +// Handle resizes +window.FramebufferResize += s => +{ + // Adjust the viewport to the new window size + gl.Viewport(s); +}; + +// Handles the dile drop and receives the array of paths to the files. +window.FileDrop += paths => { }; + +window.Update += delta => +{ + // Make sure ImGui is up-to-date + controller.Update((float) delta); + + app.Update(delta); +}; + +// The render function +window.Render += delta => +{ + // This is where you'll do any rendering beneath the ImGui context + // Here, we just have a blank screen. + gl.ClearColor( + Color.FromArgb( + 255, + (int) (.45f * 255), + (int) (.55f * 255), + (int) (.60f * 255) + ) + ); + gl.Clear((uint) ClearBufferMask.ColorBufferBit); + + app.OnImGuiRender(); + + // Make sure ImGui renders too! + controller.Render(); +}; + +// The closing function +window.Closing += () => +{ + app.Exit(); + + ImGui.SaveIniSettingsToDisk("imgui.ini"); + // Dispose our controller first + controller?.Dispose(); + + // Dispose the input context + inputContext?.Dispose(); + + // Unload OpenGL + gl?.Dispose(); +}; + +// Now that everything's defined, let's run this bad boy! +window.Run(); \ No newline at end of file diff --git a/NResUI/Utils.cs b/NResUI/Utils.cs new file mode 100644 index 0000000..aff87cb --- /dev/null +++ b/NResUI/Utils.cs @@ -0,0 +1,64 @@ +using System.Reflection; + +namespace NResUI +{ + public static class Utils + { + public static bool IsDirectory(this FileSystemInfo info) + { + // get the file attributes for file or directory + FileAttributes attr = info.Attributes; + + //detect whether its a directory or file + if ((attr & FileAttributes.Directory) == FileAttributes.Directory) + return true; + else + return false; + } + + public static bool IsDirectoryPath(this string path) + { + return Directory.Exists(path); + } + + public static void ClearContent(this DirectoryInfo directoryInfo) + { + foreach (var directory in directoryInfo.EnumerateDirectories()) + { + directory.Delete(true); + } + + foreach (var file in directoryInfo.EnumerateFiles()) + { + file.Delete(); + } + } + + public static IEnumerable GetAssignableTypesFromAssembly(Assembly assembly) + { + return assembly.ExportedTypes + .Where(t => t.IsAssignableTo(typeof(T)) && t is {IsAbstract: false, IsInterface: false}); + } + + public static IList GetAssignableTypes() + { + var executingAssembly = Assembly.GetExecutingAssembly(); + var referencedAssemblyNames = executingAssembly.GetReferencedAssemblies(); + var types = referencedAssemblyNames.SelectMany( + name => + GetAssignableTypesFromAssembly(Assembly.Load(name)) + ) + .Concat( + GetAssignableTypesFromAssembly(executingAssembly) + ) + .ToList(); + + return types; + } + + public static bool IsNullOrEmpty(this string? str) + { + return string.IsNullOrEmpty(str); + } + } +} \ No newline at end of file diff --git a/NResUI/assets/Font/OpenSans-Regular.ttf b/NResUI/assets/Font/OpenSans-Regular.ttf new file mode 100644 index 0000000..3a29f26 Binary files /dev/null and b/NResUI/assets/Font/OpenSans-Regular.ttf differ diff --git a/ParkanPlayground.sln b/ParkanPlayground.sln index d0ae5da..73783f3 100644 --- a/ParkanPlayground.sln +++ b/ParkanPlayground.sln @@ -6,6 +6,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TextureDecoder", "TextureDe EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NLUnpacker", "NLUnpacker\NLUnpacker.csproj", "{50C83E6C-23ED-4A8E-B948-89686A742CF0}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NResUI", "NResUI\NResUI.csproj", "{7456A089-0701-416C-8668-1F740BF4B72C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NResLib", "NResLib\NResLib.csproj", "{9429AEAE-80A6-4EE7-AB66-9161CC4C3A3D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -24,5 +28,13 @@ Global {50C83E6C-23ED-4A8E-B948-89686A742CF0}.Debug|Any CPU.Build.0 = Debug|Any CPU {50C83E6C-23ED-4A8E-B948-89686A742CF0}.Release|Any CPU.ActiveCfg = Release|Any CPU {50C83E6C-23ED-4A8E-B948-89686A742CF0}.Release|Any CPU.Build.0 = Release|Any CPU + {7456A089-0701-416C-8668-1F740BF4B72C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7456A089-0701-416C-8668-1F740BF4B72C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7456A089-0701-416C-8668-1F740BF4B72C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7456A089-0701-416C-8668-1F740BF4B72C}.Release|Any CPU.Build.0 = Release|Any CPU + {9429AEAE-80A6-4EE7-AB66-9161CC4C3A3D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9429AEAE-80A6-4EE7-AB66-9161CC4C3A3D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9429AEAE-80A6-4EE7-AB66-9161CC4C3A3D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9429AEAE-80A6-4EE7-AB66-9161CC4C3A3D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/ParkanPlayground/ListMetadataItem.cs b/ParkanPlayground/ListMetadataItem.cs deleted file mode 100644 index 4f87002..0000000 --- a/ParkanPlayground/ListMetadataItem.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace ParkanPlayground; - -public record ListMetadataItem(string ItemType, int ItemLength, string FileName, int OffsetInFile); \ No newline at end of file diff --git a/ParkanPlayground/ParkanPlayground.csproj b/ParkanPlayground/ParkanPlayground.csproj index 2f4fc77..d8e0c53 100644 --- a/ParkanPlayground/ParkanPlayground.csproj +++ b/ParkanPlayground/ParkanPlayground.csproj @@ -7,4 +7,8 @@ enable + + + + diff --git a/ParkanPlayground/Program.cs b/ParkanPlayground/Program.cs index 61bfb86..e7e12c8 100644 --- a/ParkanPlayground/Program.cs +++ b/ParkanPlayground/Program.cs @@ -1,105 +1,41 @@ using System.Buffers.Binary; -using System.Text;using ParkanPlayground; +using System.Text; +using NResLib; var libFile = "C:\\Program Files (x86)\\Nikita\\Iron Strategy\\ui\\ui_back.lib"; -using FileStream nResFs = new FileStream(libFile, FileMode.Open); +var parseResult = NResParser.ReadFile(libFile); -Span buffer = stackalloc byte[4]; - -nResFs.ReadExactly(buffer); - -var nResHeader = BinaryPrimitives.ReadInt32LittleEndian(buffer); - -var str = Encoding.ASCII.GetString(buffer); - -Console.WriteLine($"NRES Header: {nResHeader:X} - {str}"); - -// ---- -nResFs.ReadExactly(buffer); - -var version = BinaryPrimitives.ReadInt32LittleEndian(buffer); - -Console.WriteLine($"VERSION: {version:X}"); - -// ---- -nResFs.ReadExactly(buffer); - -var elementCount = BinaryPrimitives.ReadInt32LittleEndian(buffer); - -Console.WriteLine($"ElementCount: {elementCount}"); - -// ---- -nResFs.ReadExactly(buffer); - -var totalLength = BinaryPrimitives.ReadInt32LittleEndian(buffer); - -Console.WriteLine($"TOTAL_LENGTH: {totalLength}"); - -// ---- - -nResFs.Seek(-elementCount * 64, SeekOrigin.End); - -_ = 5; - -var elements = new List(elementCount); - -Span metaDataBuffer = stackalloc byte[64]; -for (int i = 0; i < elementCount; i++) +if (parseResult.Error != null) { - nResFs.ReadExactly(metaDataBuffer); - - var itemType = Encoding.ASCII.GetString(metaDataBuffer.Slice(0, 8)); - - var itemLength = BinaryPrimitives.ReadInt32LittleEndian(metaDataBuffer.Slice(12, 4)); - - var fileNameBlock = metaDataBuffer.Slice(20, 20); - var len = fileNameBlock.IndexOf((byte)'\0'); - if (len == -1) len = 20; // whole 20 bytes is a filename - var fileName = Encoding.ASCII.GetString(fileNameBlock.Slice(0, len)); - - var offsetInFile = BinaryPrimitives.ReadInt32LittleEndian(metaDataBuffer.Slice(56, 4)); - - var lastMagicNumber = BinaryPrimitives.ReadInt32LittleEndian(metaDataBuffer.Slice(60, 4)); - - Console.WriteLine( - $"File {i+1}: \n" + - $"\tType: {itemType}\n" + - $"\tItemLength: {itemLength}\n" + - $"\tFileName: {fileName}\n" + - $"\tOffsetInFile: {offsetInFile}\n" + - $"\tLastMagicNumber: {lastMagicNumber}" - ); - - elements.Add(new ListMetadataItem(itemType, itemLength, fileName, offsetInFile)); - - metaDataBuffer.Clear(); + Console.WriteLine(parseResult.Error); + return; } -var libFileName = new FileInfo(libFile).Name; - -if (Directory.Exists(libFileName)) -{ - Directory.Delete(libFileName, true); -} - -var dir = Directory.CreateDirectory(libFileName); - -byte[] copyBuffer = new byte[8192]; - -foreach (var element in elements) -{ - nResFs.Seek(element.OffsetInFile, SeekOrigin.Begin); - using var destFs = new FileStream(Path.Combine(libFileName, element.FileName), FileMode.CreateNew); - - var totalCopiedBytes = 0; - while (totalCopiedBytes < element.ItemLength) - { - var needReadBytes = Math.Min(element.ItemLength - totalCopiedBytes, copyBuffer.Length); - var readBytes = nResFs.Read(copyBuffer, 0, needReadBytes); - - destFs.Write(copyBuffer, 0, readBytes); - - totalCopiedBytes += readBytes; - } -} \ No newline at end of file +// var libFileName = new FileInfo(libFile).Name; +// +// if (Directory.Exists(libFileName)) +// { +// Directory.Delete(libFileName, true); +// } +// +// var dir = Directory.CreateDirectory(libFileName); +// +// byte[] copyBuffer = new byte[8192]; +// +// foreach (var element in elements) +// { +// nResFs.Seek(element.OffsetInFile, SeekOrigin.Begin); +// using var destFs = new FileStream(Path.Combine(libFileName, element.FileName), FileMode.CreateNew); +// +// var totalCopiedBytes = 0; +// while (totalCopiedBytes < element.ItemLength) +// { +// var needReadBytes = Math.Min(element.ItemLength - totalCopiedBytes, copyBuffer.Length); +// var readBytes = nResFs.Read(copyBuffer, 0, needReadBytes); +// +// destFs.Write(copyBuffer, 0, readBytes); +// +// totalCopiedBytes += readBytes; +// } +// } \ No newline at end of file