From 4ec1dc3f5d1f004c8ee96c92670d47fc3ad0949d Mon Sep 17 00:00:00 2001 From: Kizari Date: Fri, 4 Feb 2022 03:24:50 +1300 Subject: [PATCH] Asset Explorer Game View, Model Replacements Fix --- Flagrum.Blender/__init__.py | 16 +- Flagrum.Blender/globals.py | 11 + .../import_export/export_context.py | 7 + Flagrum.Blender/import_export/menu.py | 28 +- Flagrum.Blender/import_export/pack_mesh.py | 9 +- Flagrum.Blender/operations/merge_normals.py | 67 ++ Flagrum.Blender/panel/cleanup_panel.py | 30 +- Flagrum.Blender/panel/material_panel.py | 6 +- Flagrum.Blender/panel/normals_panel.py | 74 ++ Flagrum.Console/BtexTests.cs | 22 - Flagrum.Console/Program.cs | 430 +---------- Flagrum.Console/Utilities/FileFinder.cs | 123 +++ Flagrum.Core/Archive/Unpacker.cs | 41 +- Flagrum.Core/Archive/ZLibNet/Helpers.cs | 161 ++++ Flagrum.Core/Archive/ZLibNet/Minizip.cs | 698 ++++++++++++++++++ Flagrum.Core/Archive/ZLibNet/ZLib.cs | 295 ++++++++ .../Archive/ZLibNet/ZLibCompressors.cs | 170 +++++ Flagrum.Core/Archive/ZLibNet/ZLibDll.cs | 21 + Flagrum.Core/Archive/ZLibNet/ZLibStreams.cs | 445 +++++++++++ Flagrum.Core/Entities/NamedTreeNode.cs | 44 ++ Flagrum.Core/Flagrum.Core.csproj | 5 + Flagrum.Core/Gfxbin/Btex/BtexConverter.cs | 15 +- Flagrum.Core/Gfxbin/Btex/BtexHelper.cs | 15 +- Flagrum.Core/Gfxbin/Gmdl/ModelReplacer.cs | 106 ++- Flagrum.Core/Utilities/IOHelper.cs | 4 - Flagrum.Core/Utilities/Serialization.cs | 7 + Flagrum.Core/zlib64.dll | Bin 0 -> 165376 bytes Flagrum.Desktop/Flagrum.Desktop.csproj | 31 +- Flagrum.Desktop/MainWindow.xaml.cs | 6 +- .../wwwroot/app.css | 0 .../wwwroot/bmc.js | 0 .../wwwroot/discord-button.png | Bin .../wwwroot/fonts/MaterialIcons-Regular.ttf | Bin .../fonts/MaterialIconsOutlined-Regular.otf | Bin .../wwwroot/fonts/Play-Regular.ttf | Bin .../wwwroot/fonts/Roboto-Regular.ttf | Bin .../wwwroot/index.html | 23 + .../wwwroot/interop.js | 0 Flagrum.Web/App.razor | 124 +++- .../Controls/LoadingIndicator.razor | 4 + .../Controls}/ModTypeButton.razor | 0 .../Controls}/ModTypeButtonGroup.razor | 0 .../AssetExplorer/Data/AssetExplorerItem.cs | 73 ++ .../AssetExplorer/Data/ExplorerItem.cs | 1 + .../AssetExplorer/ExplorerItemRow.razor | 25 +- .../Export/BatchExportModal.razor | 110 +++ .../Export/ExportContextMenu.razor | 100 +++ .../Export/ExportFolderModal.razor | 144 ++++ .../Export/ExportWithDependenciesModal.razor | 186 +++++ .../Features/AssetExplorer/Index.razor | 374 +++++----- .../Previews/MaterialPreview.razor | 12 +- .../AssetExplorer/Previews/ModelPreview.razor | 11 +- .../AssetExplorer/Previews/Preview.razor | 29 + .../Previews/TexturePreview.razor | 34 + .../AssetExplorer/RegenerateModal.razor | 51 ++ .../AssetExplorer/VirtualExplorerRow.razor | 89 +++ .../AssetExplorer/VirtualExplorerView.razor | 23 + .../Features/AssetExplorer/_Imports.razor | 3 +- .../Features/EarcMods/EarcModCard.razor | 28 + Flagrum.Web/Features/EarcMods/Index.razor | 35 + .../Features/FileInspector/Index.razor | 61 -- .../FileInspector/ObjectInspector.razor | 93 --- Flagrum.Web/Features/ModLibrary/Index.razor | 81 +- Flagrum.Web/Flagrum.Web.csproj | 39 +- Flagrum.Web/Layout/MainLayout.razor | 17 +- Flagrum.Web/Layout/MainMenu.razor | 25 +- .../Persistence/Entities/ArchiveLocation.cs | 11 + .../Persistence/Entities/AssetExplorerNode.cs | 54 ++ Flagrum.Web/Persistence/Entities/AssetUri.cs | 28 + .../Persistence/Entities/FlagrumMod.cs | 9 + Flagrum.Web/Persistence/Entities/StatePair.cs | 52 ++ Flagrum.Web/Persistence/FlagrumDbContext.cs | 41 + .../20220129040735_Initial.Designer.cs | 102 +++ .../Migrations/20220129040735_Initial.cs | 84 +++ .../20220202142725_StatePairs.Designer.cs | 115 +++ .../Migrations/20220202142725_StatePairs.cs | 30 + .../FlagrumDbContextModelSnapshot.cs | 113 +++ Flagrum.Web/Services/AppStateService.cs | 10 +- Flagrum.Web/Services/BinmodBuilder.cs | 10 +- Flagrum.Web/Services/DependencyInjection.cs | 15 +- Flagrum.Web/Services/TextureConverter.cs | 37 +- Flagrum.Web/Services/UriMapper.cs | 108 +++ Flagrum.Web/_Imports.razor | 4 +- Flagrum.sln | 14 - 84 files changed, 4468 insertions(+), 1051 deletions(-) create mode 100644 Flagrum.Blender/globals.py create mode 100644 Flagrum.Blender/import_export/export_context.py create mode 100644 Flagrum.Blender/operations/merge_normals.py delete mode 100644 Flagrum.Console/BtexTests.cs create mode 100644 Flagrum.Console/Utilities/FileFinder.cs create mode 100644 Flagrum.Core/Archive/ZLibNet/Helpers.cs create mode 100644 Flagrum.Core/Archive/ZLibNet/Minizip.cs create mode 100644 Flagrum.Core/Archive/ZLibNet/ZLib.cs create mode 100644 Flagrum.Core/Archive/ZLibNet/ZLibCompressors.cs create mode 100644 Flagrum.Core/Archive/ZLibNet/ZLibDll.cs create mode 100644 Flagrum.Core/Archive/ZLibNet/ZLibStreams.cs create mode 100644 Flagrum.Core/Entities/NamedTreeNode.cs create mode 100644 Flagrum.Core/zlib64.dll rename {Flagrum.Web => Flagrum.Desktop}/wwwroot/app.css (100%) rename {Flagrum.Web => Flagrum.Desktop}/wwwroot/bmc.js (100%) rename {Flagrum.Web => Flagrum.Desktop}/wwwroot/discord-button.png (100%) rename {Flagrum.Web => Flagrum.Desktop}/wwwroot/fonts/MaterialIcons-Regular.ttf (100%) rename {Flagrum.Web => Flagrum.Desktop}/wwwroot/fonts/MaterialIconsOutlined-Regular.otf (100%) rename {Flagrum.Web => Flagrum.Desktop}/wwwroot/fonts/Play-Regular.ttf (100%) rename {Flagrum.Web => Flagrum.Desktop}/wwwroot/fonts/Roboto-Regular.ttf (100%) rename {Flagrum.Web => Flagrum.Desktop}/wwwroot/index.html (64%) rename {Flagrum.Web => Flagrum.Desktop}/wwwroot/interop.js (100%) create mode 100644 Flagrum.Web/Components/Controls/LoadingIndicator.razor rename Flagrum.Web/{Features/ModLibrary/Components => Components/Controls}/ModTypeButton.razor (100%) rename Flagrum.Web/{Features/ModLibrary/Components => Components/Controls}/ModTypeButtonGroup.razor (100%) create mode 100644 Flagrum.Web/Features/AssetExplorer/Data/AssetExplorerItem.cs create mode 100644 Flagrum.Web/Features/AssetExplorer/Export/BatchExportModal.razor create mode 100644 Flagrum.Web/Features/AssetExplorer/Export/ExportContextMenu.razor create mode 100644 Flagrum.Web/Features/AssetExplorer/Export/ExportFolderModal.razor create mode 100644 Flagrum.Web/Features/AssetExplorer/Export/ExportWithDependenciesModal.razor create mode 100644 Flagrum.Web/Features/AssetExplorer/Previews/Preview.razor create mode 100644 Flagrum.Web/Features/AssetExplorer/Previews/TexturePreview.razor create mode 100644 Flagrum.Web/Features/AssetExplorer/RegenerateModal.razor create mode 100644 Flagrum.Web/Features/AssetExplorer/VirtualExplorerRow.razor create mode 100644 Flagrum.Web/Features/AssetExplorer/VirtualExplorerView.razor create mode 100644 Flagrum.Web/Features/EarcMods/EarcModCard.razor create mode 100644 Flagrum.Web/Features/EarcMods/Index.razor delete mode 100644 Flagrum.Web/Features/FileInspector/Index.razor delete mode 100644 Flagrum.Web/Features/FileInspector/ObjectInspector.razor create mode 100644 Flagrum.Web/Persistence/Entities/ArchiveLocation.cs create mode 100644 Flagrum.Web/Persistence/Entities/AssetExplorerNode.cs create mode 100644 Flagrum.Web/Persistence/Entities/AssetUri.cs create mode 100644 Flagrum.Web/Persistence/Entities/FlagrumMod.cs create mode 100644 Flagrum.Web/Persistence/Entities/StatePair.cs create mode 100644 Flagrum.Web/Persistence/FlagrumDbContext.cs create mode 100644 Flagrum.Web/Persistence/Migrations/20220129040735_Initial.Designer.cs create mode 100644 Flagrum.Web/Persistence/Migrations/20220129040735_Initial.cs create mode 100644 Flagrum.Web/Persistence/Migrations/20220202142725_StatePairs.Designer.cs create mode 100644 Flagrum.Web/Persistence/Migrations/20220202142725_StatePairs.cs create mode 100644 Flagrum.Web/Persistence/Migrations/FlagrumDbContextModelSnapshot.cs create mode 100644 Flagrum.Web/Services/UriMapper.cs diff --git a/Flagrum.Blender/__init__.py b/Flagrum.Blender/__init__.py index c7682dc3..be592044 100644 --- a/Flagrum.Blender/__init__.py +++ b/Flagrum.Blender/__init__.py @@ -4,17 +4,18 @@ from bpy.utils import register_class, unregister_class from . import addon_updater_ops +from .globals import FlagrumGlobals from .import_export.menu import ImportOperator, ExportOperator from .panel.cleanup_panel import CleanupPanel, DeleteUnusedBonesOperator, DeleteUnusedVGroupsOperator, \ NormaliseWeightsOperator from .panel.material_data import MaterialSettings, FlagrumMaterialProperty, FlagrumMaterialPropertyCollection from .panel.material_panel import MaterialEditorPanel, MaterialResetOperator, TextureSlotOperator, \ ClearTextureOperator, MaterialImportOperator, MaterialCopyOperator, MaterialPasteOperator -from .panel.normals_panel import UseCustomNormalsOperator, NormalsPanel +from .panel.normals_panel import UseCustomNormalsOperator, NormalsPanel, SplitEdgesOperator, MergeNormalsOperator bl_info = { "name": "Flagrum", - "version": (1, 0, 4), + "version": (1, 0, 5), "blender": (2, 93, 0), "location": "File > Import-Export", "description": "Blender add-on for Flagrum", @@ -81,11 +82,14 @@ def draw(self, context): MaterialEditorPanel, MaterialSettings, UseCustomNormalsOperator, + SplitEdgesOperator, + MergeNormalsOperator, DeleteUnusedBonesOperator, DeleteUnusedVGroupsOperator, NormaliseWeightsOperator, CleanupPanel, - NormalsPanel + NormalsPanel, + FlagrumGlobals ) @@ -106,11 +110,13 @@ def register(): bpy.types.TOPBAR_MT_file_import.append(import_menu_item) bpy.types.TOPBAR_MT_file_export.append(export_menu_item) bpy.types.Object.flagrum_material = PointerProperty(type=MaterialSettings) - bpy.types.Scene.flagrum_material_clipboard = PointerProperty(type=FlagrumMaterialPropertyCollection) + bpy.types.WindowManager.flagrum_material_clipboard = PointerProperty(type=FlagrumMaterialPropertyCollection) + bpy.types.WindowManager.flagrum_globals = PointerProperty(type=FlagrumGlobals) def unregister(): - del bpy.types.Scene.flagrum_material_clipboard + del bpy.types.WindowManager.flagrum_globals + del bpy.types.WindowManager.flagrum_material_clipboard del bpy.types.Object.flagrum_material bpy.types.TOPBAR_MT_file_import.remove(export_menu_item) bpy.types.TOPBAR_MT_file_import.remove(import_menu_item) diff --git a/Flagrum.Blender/globals.py b/Flagrum.Blender/globals.py new file mode 100644 index 00000000..f026e8b9 --- /dev/null +++ b/Flagrum.Blender/globals.py @@ -0,0 +1,11 @@ +from bpy.props import BoolProperty +from bpy.types import PropertyGroup + + +class FlagrumGlobals(PropertyGroup): + retain_base_armature: BoolProperty( + name="Retain base armature", + description="Prevents removal of unused bones from removing base bones even if no vertices are weighted " + "to them", + default=False + ) diff --git a/Flagrum.Blender/import_export/export_context.py b/Flagrum.Blender/import_export/export_context.py new file mode 100644 index 00000000..ccaa082c --- /dev/null +++ b/Flagrum.Blender/import_export/export_context.py @@ -0,0 +1,7 @@ +from dataclasses import dataclass + + +@dataclass +class ExportContext: + smooth_normals: bool + distance: float diff --git a/Flagrum.Blender/import_export/menu.py b/Flagrum.Blender/import_export/menu.py index 3caceb89..548009eb 100644 --- a/Flagrum.Blender/import_export/menu.py +++ b/Flagrum.Blender/import_export/menu.py @@ -1,8 +1,8 @@ -import bpy -from bpy.props import StringProperty +from bpy.props import StringProperty, FloatProperty, BoolProperty from bpy.types import Operator from bpy_extras.io_utils import ImportHelper, ExportHelper +from .export_context import ExportContext from .generate_armature import generate_armature from .generate_mesh import generate_mesh from .import_context import ImportContext @@ -47,8 +47,30 @@ class ExportOperator(Operator, ExportHelper): options={'HIDDEN'} ) + smooth_normals: BoolProperty( + name="Smooth Normals on Doubles", + description="When the exporter splits edges for compatibility with FFXV, this functionality will smooth " + "the seams caused by the edge-splitting", + default=False + ) + + distance: FloatProperty( + name="Distance", + description="The maximum distance between doubles for which to merge normals", + default=0.0001, + min=0.0001, + precision=4 + ) + + def draw(self, context): + layout = self.layout + layout.label(text="FMD Export Options") + layout.prop(self, property="smooth_normals") + layout.prop(self, property="distance") + def execute(self, context): - data = pack_mesh() + export_context = ExportContext(self.smooth_normals, self.distance) + data = pack_mesh(export_context) Interop.export_mesh(self.filepath, data) return {'FINISHED'} diff --git a/Flagrum.Blender/import_export/pack_mesh.py b/Flagrum.Blender/import_export/pack_mesh.py index e1df218e..6b92a2ce 100644 --- a/Flagrum.Blender/import_export/pack_mesh.py +++ b/Flagrum.Blender/import_export/pack_mesh.py @@ -3,7 +3,9 @@ from bpy.types import Object, Mesh from mathutils import Matrix, Vector +from .export_context import ExportContext from ..entities import Gpubin, UV, Vector3, MeshData, UVMap, ColorMap, Color4, Normal, MaterialData +from ..operations.merge_normals import merge_normals from ..panel.material_data import material_weight_limit # Matrix that converts the axes back to FBX coordinate system @@ -14,7 +16,7 @@ ]) -def pack_mesh(): +def pack_mesh(export_context: ExportContext): mesh_data = Gpubin() mesh_data.Meshes = [] @@ -70,6 +72,11 @@ def pack_mesh(): bmesh.update_edit_mesh(mesh_copy.data) bpy.ops.object.mode_set(mode='OBJECT') + # Merge normals on doubles if set in export settings + if export_context.smooth_normals: + merge_normals(mesh_copy.data, export_context.distance) + + # Pack the mesh data mesh.VertexPositions = _pack_vertex_positions(mesh_copy) mesh.FaceIndices = _pack_faces(mesh_copy) mesh.UVMaps = _pack_uv_maps(mesh_copy) diff --git a/Flagrum.Blender/operations/merge_normals.py b/Flagrum.Blender/operations/merge_normals.py new file mode 100644 index 00000000..dbd55e31 --- /dev/null +++ b/Flagrum.Blender/operations/merge_normals.py @@ -0,0 +1,67 @@ +from bpy.types import Mesh +from mathutils import Vector + + +def merge_normals(mesh: Mesh, distance: float): + new_normals = {} + + # Split vertices up into a grid by the set merge distance + grid = {} + for vertex in mesh.vertices: + x = vertex.co[0] // distance + y = vertex.co[1] // distance + z = vertex.co[2] // distance + + if x not in grid: + grid[x] = {} + if y not in grid[x]: + grid[x][y] = {} + if z not in grid[x][y]: + grid[x][y][z] = [] + + grid[x][y][z].append(vertex) + + # Merge custom normals for vertices in range + vertices = set(vertex for vertex in mesh.vertices) + while vertices: + vertex = vertices.pop() + vertex_normal = Vector() + x = vertex.co[0] // distance + y = vertex.co[1] // distance + z = vertex.co[2] // distance + + # Find list of all vertices in adjacent parts of the grid + adjacent = [] + for i in [x - 1, x, x + 1]: + for j in [y - 1, y, y + 1]: + for k in [z - 1, z, z + 1]: + if i in grid and j in grid[i] and k in grid[i][j]: + adjacent.extend(grid[i][j][k]) + + # Remove vertices that are out of the merge distance + in_range = [v for v in adjacent if (v.co - vertex.co).length_squared <= distance ** 2] + + # Average all the normals of the vertices in range + for other in in_range: + vertex_normal += other.normal + + vertex_normal.normalize() + + # Add new normals for merged vertices + for other in in_range: + new_normals[other.index] = vertex_normal + + # Remove merged vertices from the set + processed = [v for v in in_range] + vertices.difference_update(processed) + + # Apply new normals to the mesh + normals = [] + for i in range(len(mesh.vertices)): + if i in new_normals: + normals.append([new_normals[i].x, new_normals[i].y, new_normals[i].z]) + else: + normals.append(mesh.vertices[i].normal) + + mesh.normals_split_custom_set_from_vertices(normals) + mesh.use_auto_smooth = True diff --git a/Flagrum.Blender/panel/cleanup_panel.py b/Flagrum.Blender/panel/cleanup_panel.py index 96a7c97c..e5cf4215 100644 --- a/Flagrum.Blender/panel/cleanup_panel.py +++ b/Flagrum.Blender/panel/cleanup_panel.py @@ -8,7 +8,7 @@ class NormaliseWeightsOperator(Operator): bl_idname = "flagrum.cleanup_normalise_weights" bl_label = "Normalise Weights" bl_description = "Normalises vertex weights to the limits defined by the selected Flagrum materials to " \ - "ensure a consistent result with the FMD exporter" + "ensure a consistent result with the FMD exporter" @classmethod def poll(cls, context): @@ -93,6 +93,29 @@ def execute(self, context): meshes.append(obj) bones_to_keep = ["C_Hip"] + if context.window_manager.flagrum_globals.retain_base_armature: + bones_to_keep = ["C_Hip", "C_Spine1", "C_Spine1Sub", "C_Spine2", "C_Spine2W", "C_Spine3", "C_Spine3W", + "C_Neck1", "C_Head", "Facial_A", "C_HairRoot", "C_HeadEnd", "Facial_B", "C_Throat_B", + "C_NeckSub", "C_NeckSubEnd", "C_Neck1W", "C_Neck1WEnd", "L_Shoulder", "L_Forearm", + "L_Hand", "L_Socket", "L_Thumb1", "L_Thumb2", "L_Thumb3", "L_ThumbEnd", "L_Index1", + "L_Index2", "L_Index3", "L_IndexBulge", "L_Middle1", "L_Middle2", "L_Middle3", + "L_MiddleBulge", "L_RingMeta", "L_Ring1", "L_Ring2", "L_Ring3", "L_RingBulge", "L_RingSub", + "L_PinkyMeta", "L_Pinky1", "L_Pinky2", "L_Pinky3", "L_PinkyBulge", "L_PinkySub", + "L_IndexSub", "L_MiddleSub", "L_ForearmrollA", "L_ForearmrollB", "L_ForearmrollC", + "L_Wrist", "L_sleeveSub", "L_DeltoidA", "L_DeltoidB", "L_DeltoidC", "L_Elbow", "L_Bust", + "L_armpit", "L_ShoulderSub", "R_Shoulder", "R_UpperArm", "R_Forearm", "R_Hand", "R_Socket", + "R_Thumb1", "R_Thumb2", "R_Thumb3", "R_ThumbEnd", "R_Index1", "R_Index2", "R_Index3", + "R_IndexBulge", "R_Middle1", "R_Middle2", "R_Middle3", "R_MiddleBulge", "R_RingMeta", + "R_Ring1", "R_Ring2", "R_Ring3", "R_RingSub", "R_PinkyMeta", "R_Pinky1", "R_Pinky2", + "R_Pinky3", "R_PinkyBulge", "R_PinkySub", "R_IndexSub", "R_MiddleSub", "R_ForearmrollA", + "R_ForearmrollB", "R_ForearmrollC", "R_Wrist", "R_sleeveSub", "R_DeltoidA", "R_DeltoidB", + "R_DeltoidC", "R_Elbow", "R_Bust", "R_ShoulderSub", "R_armpit", "L_UpperLeg", "L_Foreleg", + "L_Foot", "L_Toe", "L_ToeEnd", "L_CalfB", "L_CalfF", "L_ankle", "L_ankleB", "L_FemorisA", + "L_FemorisAsub", "L_FemorisB", "L_FemorisC", "L_Knee", "L_UpperLegSub", "L_UpperLegSubEnd", + "R_UpperLeg", "R_Foreleg", "R_Foot", "R_Toe", "R_ToeEnd", "R_CalfB", "R_CalfF", "R_ankle", + "R_FemorisA", "R_FemorisAsub", "R_FemorisB", "R_FemorisC", "R_Knee", "C_HipW", "C_Spine1W", + "C_Spine1WEnd", "L_Hip", "L_Hipback", "L_HipSub", "R_Hip", "R_Hipback", "R_HipSub", + "R_HipSubEnd", "c_BeltKdi"] for mesh in meshes: groups = {i: False for i, k in enumerate(mesh.vertex_groups)} @@ -127,6 +150,9 @@ class CleanupPanel(Panel): def draw(self, context): layout = self.layout - layout.operator(DeleteUnusedBonesOperator.bl_idname) + row = layout.row(align=True) + row.operator(DeleteUnusedBonesOperator.bl_idname) + row.prop(context.window_manager.flagrum_globals, property="retain_base_armature", icon='OUTLINER_OB_ARMATURE', + icon_only=True) layout.operator(DeleteUnusedVGroupsOperator.bl_idname) layout.operator(NormaliseWeightsOperator.bl_idname) diff --git a/Flagrum.Blender/panel/material_panel.py b/Flagrum.Blender/panel/material_panel.py index a426dca5..6222d532 100644 --- a/Flagrum.Blender/panel/material_panel.py +++ b/Flagrum.Blender/panel/material_panel.py @@ -111,7 +111,7 @@ class MaterialCopyOperator(Operator): bl_label = "Copy" def execute(self, context): - clipboard = context.scene.flagrum_material_clipboard + clipboard = context.window_manager.flagrum_material_clipboard material: MaterialSettings = context.view_layer.objects.active.flagrum_material active_material_data = None for property_definition in material.property_collection: @@ -132,7 +132,7 @@ class MaterialPasteOperator(Operator): @classmethod def poll(cls, context): - material_id = context.scene.flagrum_material_clipboard.material_id + material_id = context.window_manager.flagrum_material_clipboard.material_id material: MaterialSettings = context.view_layer.objects.active.flagrum_material active_material_data = None for property_definition in material.property_collection: @@ -141,7 +141,7 @@ def poll(cls, context): return material_id is not None and material_id != '' and active_material_data.material_id == material_id def execute(self, context): - clipboard = context.scene.flagrum_material_clipboard + clipboard = context.window_manager.flagrum_material_clipboard material: MaterialSettings = context.view_layer.objects.active.flagrum_material active_material_data = None for property_definition in material.property_collection: diff --git a/Flagrum.Blender/panel/normals_panel.py b/Flagrum.Blender/panel/normals_panel.py index 88b66f62..d8ce4ac6 100644 --- a/Flagrum.Blender/panel/normals_panel.py +++ b/Flagrum.Blender/panel/normals_panel.py @@ -1,7 +1,11 @@ from array import array +import bmesh +import bpy from bpy.types import Panel, Operator, Mesh +from ..operations.merge_normals import merge_normals + class UseCustomNormalsOperator(Operator): bl_idname = "flagrum.use_custom_normals" @@ -51,6 +55,74 @@ def execute(self, context): return {'FINISHED'} +class SplitEdgesOperator(Operator): + bl_idname = "flagrum.split_edges" + bl_label = "Split Edges" + bl_description = "Creates doubles along UV boundaries for compatibility with Luminous" + + @classmethod + def poll(cls, context): + selected_meshes = [] + for obj in context.view_layer.objects.selected: + if obj.type == 'MESH': + selected_meshes.append(obj) + return len(selected_meshes) > 0 + + def execute(self, context): + for obj in context.view_layer.objects.selected: + if obj.type == 'MESH': + # Make sure all verts are selected otherwise some of the bmesh operations shit themselves + for vertex in obj.data.vertices: + vertex.select = True + + bpy.ops.object.mode_set(mode='EDIT') + bmesh_copy = bmesh.from_edit_mesh(obj.data) + + # Clear seams as we need to use them for splitting + for edge in bmesh_copy.edges: + if edge.seam: + edge.seam = False + + # Select all UV verts as seams_from_islands relies on this to function + uv_layer = bmesh_copy.loops.layers.uv.verify() + for face in bmesh_copy.faces: + for loop in face.loops: + loop_uv = loop[uv_layer] + loop_uv.select = True + + # Split edges + bpy.ops.uv.seams_from_islands() + island_boundaries = [edge for edge in bmesh_copy.edges if edge.seam and len(edge.link_faces) == 2] + bmesh.ops.split_edges(bmesh_copy, edges=island_boundaries) + + # Apply the changes to the mesh + bmesh.update_edit_mesh(obj.data) + bpy.ops.object.mode_set(mode='OBJECT') + + return {'FINISHED'} + + +class MergeNormalsOperator(Operator): + bl_idname = "flagrum.merge_normals" + bl_label = "Merge Normals" + bl_description = "Averages normals across doubles on selected meshes to smooth out the shading along seams" + + @classmethod + def poll(cls, context): + selected_meshes = [] + for obj in context.view_layer.objects.selected: + if obj.type == 'MESH': + selected_meshes.append(obj) + return len(selected_meshes) > 0 + + def execute(self, context): + for obj in context.view_layer.objects.selected: + if obj.type == 'MESH': + merge_normals(obj.data, 0.0001) + + return {'FINISHED'} + + class NormalsPanel(Panel): bl_idname = "VIEW_3D_PT_flagrum_normals" bl_label = "Custom Normals" @@ -61,3 +133,5 @@ class NormalsPanel(Panel): def draw(self, context): layout = self.layout layout.operator(UseCustomNormalsOperator.bl_idname) + layout.operator(SplitEdgesOperator.bl_idname) + layout.operator(MergeNormalsOperator.bl_idname) diff --git a/Flagrum.Console/BtexTests.cs b/Flagrum.Console/BtexTests.cs deleted file mode 100644 index 45af1d47..00000000 --- a/Flagrum.Console/BtexTests.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using System.IO; -using Flagrum.Core.Gfxbin.Btex; -using Flagrum.Web.Services; - -namespace Flagrum.Console; - -public static class BtexTests -{ - public static void Convert() - { - var name = "body_o"; - var timer = DateTime.Now; - var inputPath = $"C:\\Modding\\BTex\\{name}.png"; - var outputPath = $"C:\\Modding\\BTex\\{name}.btex"; - - var converter = new TextureConverter(); - var btex = converter.ToBtex(name, "png", TextureType.Greyscale, File.ReadAllBytes(inputPath)); - File.WriteAllBytes(outputPath, btex); - System.Console.WriteLine((DateTime.Now - timer).TotalMilliseconds); - } -} \ No newline at end of file diff --git a/Flagrum.Console/Program.cs b/Flagrum.Console/Program.cs index 51bdb6c9..6adec852 100644 --- a/Flagrum.Console/Program.cs +++ b/Flagrum.Console/Program.cs @@ -1,409 +1,61 @@ -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using Flagrum.Console.Utilities; -using Flagrum.Core.Gfxbin.Gmtl.Data; -using Newtonsoft.Json; +using System.IO; +using Flagrum.Core.Gfxbin.Gmdl; namespace Flagrum.Console; public class Program { - private static void EnumerateFilesRecursively(string directory, List<(string path, string uri)> paths) - { - foreach (var subDirectory in Directory.EnumerateDirectories(directory)) - { - EnumerateFilesRecursively(subDirectory, paths); - } - - paths.AddRange(Directory.EnumerateFiles(directory).Select(f => - { - var result = f.Replace("C:\\Testing\\ModelReplacements\\mo-sword\\", "data://").Replace('\\', '/'); - return (f, result.Contains("/wetness/") - ? result.Replace(".btex", ".tga") - : result.Replace(".btex", ".tif")); - })); - } - public static void Main(string[] args) { - //var json = File.ReadAllText(@"C:\Code\Flagrum\Flagrum.Blender\templates\LUCII_PHANTOM.json"); - var jsonBytes = File.ReadAllBytes(@"C:\Modding\MaterialTesting\glass_sword\LUCII_PHANTOM.json"); - var json = Encoding.UTF8.GetString(jsonBytes); - var test = JsonConvert.DeserializeObject(json); - //const string templateName = "LUCII_PHANTOM"; - //MaterialToPython.ConvertFromJsonFile(@$"C:\Code\Flagrum\Flagrum.Blender\templates\{templateName}.json", - // @$"C:\Modding\MaterialTesting\{templateName}.py"); - - // var json = File.ReadAllText(@"C:\Modding\normals.txt"); - // var normals = JsonConvert.DeserializeObject>(json); - // - // var gfx = @"C:\Users\Kieran\Desktop\character\nh\nh02\model_000\nh02_000.gmdl.gfxbin"; - // var gpu = gfx.Replace(".gmdl.gfxbin", ".gpubin"); - // var reader = new ModelReader(File.ReadAllBytes(gfx), File.ReadAllBytes(gpu)); - // var model = reader.Read(); + var gfx = File.ReadAllBytes(@"C:\Users\Kieran\Desktop\Models2\nh00\mod\seams_test.gmdl.gfxbin"); + var gpu = File.ReadAllBytes(@"C:\Users\Kieran\Desktop\Models2\nh00\mod\seams_test.gpubin"); + var model = new ModelReader(gfx, gpu).Read(); + var x = true; + //Visit(@"C:\Program Files (x86)\Steam\steamapps\common\FINAL FANTASY XV\datas"); + // var json = File.ReadAllText(@"C:\Modding\map3.json"); + // var map = JsonConvert.DeserializeObject>>(json); // - // var normals2 = model.MeshObjects[0].Meshes - // .Where(m => m.Name == "EyelashShape" && m.LodNear == 0) - // .SelectMany(m => m.Tangents - // .Select(n => new int[] {n.X, n.Y, n.Z, n.W})) - // .ToList(); + // var start = DateTime.Now; // - // System.Console.WriteLine($"Original count: {normals2.Count()}"); - // System.Console.WriteLine($"New count: {normals.Count()}"); - // - // var matching = 0; - // var notMatching = 0; - // for (var i = 0; i < normals2.Count(); i++) + // var root = new NamedTreeNode("data://", null); + // foreach (var (archive, uris) in map) // { - // if (Math.Abs(normals[i][0] - normals2[i][0]) < 5 - // && Math.Abs(normals[i][1] - normals2[i][1]) < 5 - // && Math.Abs(normals[i][2] - normals2[i][2]) < 5 - // && Math.Abs(normals[i][3] - normals2[i][3]) < 5) - // { - // matching++; - // } - // else + // foreach (var uri in uris) // { - // notMatching++; - // } - // } - // - // System.Console.WriteLine($"Matching: {matching}"); - // System.Console.WriteLine($"Not Matching: {notMatching}"); - - // using var unpacker = new Unpacker(@"C:\Program Files (x86)\Steam\steamapps\common\FINAL FANTASY XV\datas\character\am\am80\model_007\autoexternal.earc"); - // var gfx = unpacker.UnpackFileByQuery("am80_007.gmdl", out _); - // var gpu = unpacker.UnpackFileByQuery("am80_007.gpubin", out _); - // var model = new ModelReader(gfx, gpu).Read(); - // foreach (var mesh in model.MeshObjects[0].Meshes) - // { - // System.Console.WriteLine(mesh.Name); - // } - - - //AOFixer.Run(); - //new MaterialFinder().MakeTemplate(); - //MaterialToPython.ConvertFromJsonFile(@"C:\Modding\MaterialTesting\NAMED_HUMAN_GLASS.json", - // @"C:\Modding\MaterialTesting\NAMED_HUMAN_GLASS.py"); - //MaterialToPython.Convert(@"C:\Modding\nh03_000_basic_01_mat.gmtl.gfxbin", @"C:\Modding\glass.py"); - //GfxbinTests.CheckMaterialDefaults(@"C:\Modding\nh05_000_skin_00_mat.gmtl.gfxbin"); - // using var unpacker = new Unpacker(@"C:\Program Files (x86)\Steam\steamapps\common\FINAL FANTASY XV\datas\character\me\me01\model_010\materials\autoexternal.earc"); - // var gmtls = unpacker.UnpackFilesByQuery(".gmtl"); - // foreach (var (name, data) in gmtls) - // { - // var reader = new MaterialReader(data); - // var material = reader.Read(); - // } - // bool x = true; - //var gfx = @"C:\Users\Kieran\Desktop\character\aw\aw90\model_010\aw90_010.gmdl.gfxbin"; - //var gpu = gfx.Replace(".gmdl.gfxbin", ".gpubin"); - //var reader = new ModelReader(File.ReadAllBytes(gfx), File.ReadAllBytes(gpu)); - //var model = reader.Read(); - //bool x = true; - //MaterialsToTemplates.Run(); - //GfxbinTests.CheckMaterial(); - // GfxbinTests.CheckModel(); - //GfxbinTests.CheckMaterialDefaults(@"C:\Users\Kieran\Downloads\me01_010_monbasic_mat_00.gmtl.gfxbin"); - // GfxbinTests.CheckMaterialDefaults(@"C:\Modding\nh01_010_skin_00_mat.gmtl.gfxbin"); - // GfxbinTests.CheckMaterialDefaults(@"C:\Modding\nh02_010_skin_01_mat.gmtl.gfxbin"); - // GfxbinTests.CheckMaterialDefaults(@"C:\Modding\nh03_010_skin_00_mat.gmtl.gfxbin"); - - // var gfx = @"C:\Modding\Extractions\character\am\am50\model_010\am50_010.gmdl.gfxbin"; - // var gpu = gfx.Replace(".gmdl.gfxbin", ".gpubin"); - // var reader = new ModelReader(File.ReadAllBytes(gfx), File.ReadAllBytes(gpu)); - // var model = reader.Read(); - // bool x = true; - - // var gfx = @"C:\Modding\character\am\am00\model_001\am00_001.gmdl.gfxbin"; - // var gpu = gfx.Replace(".gmdl.gfxbin", ".gpubin"); - // var reader = new ModelReader(File.ReadAllBytes(gfx), File.ReadAllBytes(gpu)); - // var model = reader.Read(); - // var builder = new StringBuilder(); - // builder.AppendLine("new List"); - // builder.AppendLine("{"); - // foreach (var bone in model.BoneHeaders) - // { - // builder.AppendLine(" new() { Name = \"" + bone.Name + "\", LodIndex = " + bone.LodIndex + " },"); - // } - // - // builder.AppendLine("};"); - // File.WriteAllText(@"C:\Modding\boneTable.cs", builder.ToString()); - - //BtexTests.Convert(); - // var path = $"{IOHelper.GetExecutingDirectory()}\\Resources\\Materials\\BASIC_MATERIAL.json"; - // var material = JsonConvert.DeserializeObject(File.ReadAllText(path)); - // - // material.Textures.Add(new MaterialTexture - // { - // ResourceFileHash = 2420523974290568125, - // Name = "Emissive0_Texture_TEX_Material0_", - // NameHash = 255212429, - // ShaderGenName = "Emissive0_Texture", - // ShaderGenNameHash = 3822196702, - // Path = "data://shader/defaulttextures/white.tif", - // PathHash = 3705256527, - // Flags = 1, - // HighTextureStreamingLevels = 0, - // Unknown2 = 1662278111 - // }); - // - // material.Textures.Add(new MaterialTexture - // { - // ResourceFileHash = 2420523974290568125, - // Name = "OpacityMask0_Texture_TEX_Material0_", - // NameHash = 1122382789, - // ShaderGenName = "OpacityMask0_Texture", - // ShaderGenNameHash = 3887834006, - // Path = "data://shader/defaulttextures/white.tif", - // PathHash = 3705256527, - // Flags = 1, - // HighTextureStreamingLevels = 0, - // Unknown2 = 1446710005 - // }); - // var result = JsonConvert.SerializeObject(material); - // File.WriteAllText("C:\\Modding\\BASIC_MATERIAL.json", result); - - // var gfx = "C:\\Users\\Kieran\\Desktop\\Mods\\Noctis\\character\\nh\\nh00\\model_010\\nh00_010.gmdl.gfxbin"; - // //var gfx = "C:\\Modding\\ModelReplacementTesting\\mod\\gladio_succulent\\mo00_001.gmdl.gfxbin"; - // var gpu = gfx.Replace(".gmdl.gfxbin", ".gpubin"); - // var reader = new ModelReader(File.ReadAllBytes(gfx), File.ReadAllBytes(gpu)); - // var model = reader.Read(); - // var json = JsonConvert.SerializeObject(model.BoneHeaders); - // File.WriteAllText("C:\\Modding\\ModelReplacementTesting\\bones.json", json); - // var x = true; - - //GfxbinTests.BuildMod2(); - // var gfx = "C:\\Modding\\Extractions\\angery_sword\\mod\\0e664ae0-1baa-4f96-8518-ad16d11d1141\\angery_sword.gmdl.gfxbin"; - // var gpu = gfx.Replace(".gmdl.gfxbin", ".gpubin"); - // var reader = new ModelReader(File.ReadAllBytes(gfx), File.ReadAllBytes(gpu)); - // var model = reader.Read(); - // bool x = true; - - // var ebex = "C:\\Testing\\ModelReplacements\\SinglePlayerSword\\temp.ebex"; - // var previewBtex = - // "C:\\Testing\\ModelReplacements\\SwordExtract\\mod\\3b7e2ca5-1a23-4afb-af38-d5726c190841\\$preview.btex"; - // var previewPng = - // "C:\\Testing\\ModelReplacements\\SwordExtract\\mod\\3b7e2ca5-1a23-4afb-af38-d5726c190841\\$preview.png.bin"; - // var modmeta = "C:\\Testing\\ModelReplacements\\SwordExtract\\mod\\3b7e2ca5-1a23-4afb-af38-d5726c190841\\index.modmeta"; - // var unpacker = new Unpacker("C:\\Modding\\Extractions\\7dc925b8-aa5c-4a17-a20f-1d5ba9921f36.earc"); - // var packer = unpacker.ToPacker(); - // - // - // var shaderDirectory = "C:\\Testing\\ModelReplacements\\mo-sword\\shader"; - // var paths = new List<(string path, string uri)>(); - // EnumerateFilesRecursively(shaderDirectory, paths); - // foreach (var (path, uri) in paths) - // { - // packer.AddFile(File.ReadAllBytes(path), uri); - // } - // - // var texturePath = "C:\\Testing\\ModelReplacements\\mo-sword\\mod\\sword_1\\khopesh_d.btex"; - // packer.AddFile(File.ReadAllBytes(texturePath), "data://mod/7dc925b8-aa5c-4a17-a20f-1d5ba9921f36/khopesh_d.png"); - // - // var materialPath = "C:\\Testing\\ModelReplacements\\mo-sword\\mod\\sword_1\\khopesh.fbxgmtl\\material.gmtl.gfxbin"; - // var reader = new MaterialReader(materialPath); - // var material = reader.Read(); - // bool x = true; - // material.Name = "body_ashape_mat"; - // material.NameHash = Cryptography.Hash32(material.Name); - // var assetUri = material.Header.Dependencies.FirstOrDefault(d => d.PathHash == "asset_uri"); - // var reference = material.Header.Dependencies.FirstOrDefault(d => d.PathHash == "ref"); - // assetUri.Path = $"data://mod/7dc925b8-aa5c-4a17-a20f-1d5ba9921f36/materials/"; - // reference.Path = $"data://mod/7dc925b8-aa5c-4a17-a20f-1d5ba9921f36/materials/body_ashape_mat.gmtl"; - // var texture = material.Textures.FirstOrDefault(t => t.Path.Contains("khopesh_d")); - // texture.Path = $"data://mod/7dc925b8-aa5c-4a17-a20f-1d5ba9921f36/khopesh_d.png"; - // texture.PathHash = Cryptography.Hash32(texture.Path); - // texture.ResourceFileHash = Cryptography.HashFileUri64(texture.Path); - // var textureDependency = material.Header.Dependencies.FirstOrDefault(d => d.Path.Contains("khopesh_d.png")); - // textureDependency.Path = texture.Path; - // var index = material.Header.Hashes.IndexOf(ulong.Parse(textureDependency.PathHash)); - // textureDependency.PathHash = texture.ResourceFileHash.ToString(); - // material.Header.Hashes[index] = texture.ResourceFileHash; - // - // var writer = new MaterialWriter(material); - // packer.UpdateFile("body_ashape_mat.gmtl", writer.Write()); - // // packer.UpdateFile("temp.ebex", File.ReadAllBytes(ebex)); - // // packer.UpdateFile("$preview.btex", File.ReadAllBytes(previewBtex)); - // // packer.UpdateFile("$preview.png.bin", File.ReadAllBytes(previewPng)); - // // packer.UpdateFile("index.modmeta", File.ReadAllBytes(modmeta)); - // packer.WriteToFile("C:\\Modding\\Extractions\\7dc925b8-aa5c-4a17-a20f-1d5ba9921f36.ffxvbinmod"); - - - // var reader = - // new MaterialReader( - // @"C:\Modding\Extractions\character\am\am50\model_010\materials\am50_010_cloth_00_mat.gmtl.gfxbin"); - // - // var material = reader.Read(); - // bool x = true; - // - // foreach (var dependency in material.Header.Dependencies) - // { - // System.Console.WriteLine(dependency.Path); - // } - - // var gfx = "C:\\Testing\\ModelReplacements\\mo-sword\\mod\\sword_1\\khopesh.gmdl.gfxbin"; - // var gpu = gfx.Replace(".gmdl.gfxbin", ".gpubin"); - // var reader = new ModelReader(File.ReadAllBytes(gfx), File.ReadAllBytes(gpu)); - // var model = reader.Read(); - // bool x = true; - // var allIndices = model.MeshObjects[0].Meshes - // .SelectMany(m => m.WeightIndices.SelectMany(n => n.SelectMany(o => o.Select(p => p)))) - // .Distinct(); - //bool x = true; - // var templatePath = $"{IOHelper.GetExecutingDirectory()}\\Resources\\Materials\\ENEMY_SKIN.json"; - // var json = File.ReadAllText(templatePath); - // var material = JsonConvert.DeserializeObject(json); - // bool x = true; - - //MaterialsToTemplates.Run(); - // var gfx = "C:\\Testing\\ModelReplacements\\mo-sword\\mod\\sword_1\\khopesh.gmdl.gfxbin"; - // var gpu = gfx.Replace(".gmdl.gfxbin", ".gpubin"); - // var reader = new ModelReader(File.ReadAllBytes(gfx), File.ReadAllBytes(gpu)); - // var model = reader.Read(); - // foreach (var node in model.NodeTable) - // { - // System.Console.WriteLine(node.Name); - // for (int i = 0; i < node.Matrix.Rows.Count; i++) - // { - // System.Console.WriteLine($"{node.Matrix.Rows[i].X}, {node.Matrix.Rows[i].Y}, {node.Matrix.Rows[i].Z}"); - // } - // } - // - // return; - - // var gfx = - // "C:\\Testing\\ModelReplacements\\Extract3\\mod\\gladiolus_ardyn\\ardynmankini.fbxgmtl\\chest.gmtl.gfxbin"; - // - // var reader = new MaterialReader(gfx); - // var material = reader.Read(); - // foreach (var texture in material.Textures) - // { - // System.Console.WriteLine(texture.Name + ": " + texture.Path); - // } - // - // System.Console.WriteLine(material.NameHash); - // System.Console.WriteLine(material.Name); - // System.Console.WriteLine(Cryptography.Hash32(material.Name)); - // - // foreach (var texture in material.Textures) - // { - // System.Console.WriteLine("\n"); - // System.Console.WriteLine($"{texture.PathHash}: {texture.Path}"); - // System.Console.WriteLine($"{Cryptography.Hash32(texture.Path)}: {texture.Path}"); - // System.Console.WriteLine($"{texture.NameHash}: {texture.Name}"); - // System.Console.WriteLine($"{Cryptography.Hash32(texture.Name)}: {texture.Name}"); - // System.Console.WriteLine($"{texture.ResourceFileHash}"); - // System.Console.WriteLine($"{Cryptography.HashFileUri64(texture.Path)}"); - // System.Console.WriteLine($"{texture.ShaderGenNameHash}: {texture.ShaderGenName}"); - // System.Console.WriteLine($"{Cryptography.Hash32(texture.ShaderGenName)}: {texture.ShaderGenName}"); - // } - - //var gfx = "C:\\Testing\\ModelReplacements\\Extract3\\mod\\gladiolus_ardyn\\ardynmankini.fbxgmtl\\chest.gmtl.gfxbin"; - // var gfx = - // "C:\\Users\\Kieran\\Desktop\\character\\nh\\nh00\\model_000\\materials\\nh00_000_skin_02_mat.gmtl.gfxbin"; - // var reader = new MaterialReader(gfx); - // var material = reader.Read(); - // - // var dependencies = new List(); - // dependencies.AddRange(material.ShaderBinaries.Where(s => s.ResourceFileHash > 0).Select(s => new DependencyPath { Path = s.Path, PathHash = s.ResourceFileHash.ToString() })); - // dependencies.AddRange(material.Textures.Where(s => s.ResourceFileHash > 0).Select(s => new DependencyPath { Path = s.Path, PathHash = s.ResourceFileHash.ToString() })); - // dependencies.Add(new DependencyPath { PathHash = "asset_uri", Path = $"data://character/nh/nh00/model_000/materials/"}); - // dependencies.Add(new DependencyPath { PathHash = "ref", Path = $"data://character/nh/nh00/model_000/materials/nh00_000_skin_02_mat.gmtl"}); - // material.Header.Dependencies = dependencies.DistinctBy(d => d.PathHash).ToList(); - // - // var writer = new MaterialWriter(material); - // File.WriteAllBytes( - // "C:\\Users\\Kieran\\Desktop\\character\\nh\\nh00\\model_000\\materials\\nh00_000_skin_02_mat_copy.gmtl.gfxbin", - // writer.Write()); - - - // var gfx = - // "C:\\Testing\\ModelReplacements\\Extract3\\mod\\gladiolus_ardyn\\ardynmankini.gmdl.gfxbin"; - // var gpu = gfx.Replace(".gmdl.gfxbin", ".gpubin"); - // - // var reader = new ModelReader(File.ReadAllBytes(gfx), File.ReadAllBytes(gpu)); - // var model = reader.Read(); - // foreach (var dependency in model.Header.Dependencies) - // { - // if (dependency.PathHash != "ref" && dependency.PathHash != "asset_uri") - // { - // dependency.PathHash = Cryptography.HashFileUri64(dependency.Path).ToString(); - // } - // } - // var writer = new ModelWriter(model); - // var (gfxData, gpuData) = writer.Write(); - // File.WriteAllBytes("C:\\Testing\\ModelReplacements\\Extract3\\mod\\gladiolus_ardyn\\ardynmankini_copy.gmdl.gfxbin", gfxData); - // File.WriteAllBytes("C:\\Testing\\ModelReplacements\\Extract3\\mod\\gladiolus_ardyn\\ardynmankini_copy.gpubin", gpuData); - - // foreach (var mesh in model.MeshObjects[0].Meshes) - // { - // System.Console.WriteLine(mesh.DefaultMaterialHash); - // System.Console.WriteLine(Cryptography.HashFileUri64($"data://mod/gladiolus_ardyn/ardynmankini.fbxgmtl/{mesh.Name}.gmtl")); - // } - // var builder = new StringBuilder(); - // builder.AppendLine("var dictionary = new Dictionary"); - // builder.AppendLine("{"); - // foreach (var dependency in model.Header.Dependencies) - // { - // if (ulong.TryParse(dependency.PathHash, out var pathHash)) - // { - // builder.Append(" {" + pathHash + ", \"" + dependency.Path + "\"}"); - // if (dependency != model.Header.Dependencies.Last()) + // var tokens = uri.Split('/'); + // var currentDirectory = root; + // foreach (var token in tokens) // { - // builder.Append(","); - // } + // var subdirectory = currentDirectory.Children + // .FirstOrDefault(c => c.Name == token) ?? currentDirectory.AddChild(token); // - // builder.Append("\n"); + // currentDirectory = subdirectory; + // } // } // } // - // builder.AppendLine("};"); - // File.WriteAllText("C:\\Modding\\Dependencies.cs", builder.ToString()); - - - //ModelReplacementTableToCs.Run(); - //GfxbinTests.GetBoneTable(); - //GfxbinTests.CheckMaterialDefaults(); - //var gfxbin = "C:\\Testing\\character\\nh\\nh01\\model_000\\materials\\nh01_000_skin_00_mat.gmtl.gfxbin"; - //var materialReader = new MaterialReader(gfxbin); - //var material = materialReader.Read(); - - // GfxbinToBoneDictionary.Run("C:\\Testing\\character\\nh\\nh03\\model_000\\nh03_000.gmdl.gfxbin"); - // var gfxbinData = - // File.ReadAllBytes( - // "C:\\Users\\Kieran\\Desktop\\Mods\\Noctis\\character\\nh\\nh00\\model_010\\nh00_010.gmdl.gfxbin"); - // var gpubinData = - // File.ReadAllBytes( - // "C:\\Users\\Kieran\\Desktop\\Mods\\Noctis\\character\\nh\\nh00\\model_010\\nh00_010.gpubin"); - // - // var reader = new ModelReader(gfxbinData, gpubinData); - // var model = reader.Read(); + // var serializer = new DataContractSerializer(typeof(NamedTreeNode)); + // using var fileStream = new FileStream(@"C:\Modding\Tree.xml", FileMode.Create, FileAccess.Write); + // serializer.WriteObject(fileStream, root); // - // var mesh = model.MeshObjects[0].Meshes.FirstOrDefault(m => m.Name == "bodyShape" && m.LodNear == 0); - // var weights = mesh.WeightValues[1].Where(w => w[0] > 0).Select(w => mesh.WeightValues[1].IndexOf(w)).Take(10); - // - // foreach (var i in weights) - // { - // var weight1 = mesh.WeightValues[0][i]; - // var weight2 = mesh.WeightValues[1][i]; - // - // System.Console.WriteLine($"[{weight1[0]}, {weight1[1]}, {weight1[2]}, {weight1[3]}]"); - // System.Console.WriteLine($"[{weight2[0]}, {weight2[1]}, {weight2[2]}, {weight2[3]}]"); - // System.Console.WriteLine( - // $"{weight1.Sum(w => w)} + {weight2.Sum(w => w)} = {weight1.Sum(w => w) + weight2.Sum(w => w)}\n\n"); - // } + // //File.WriteAllText(@"C:\Modding\UriDirectoryMap.json", JsonConvert.SerializeObject(root)); + // System.Console.WriteLine((DateTime.Now - start).TotalMilliseconds); + } - // return; - // MaterialToPython.Convert("C:\\Users\\Kieran\\Downloads\\nh02_000_skin_01_mat.gmtl.gfxbin", - // "C:\\Testing\\NewHumanSkin.py"); - // return; - // //var reader = new MaterialReader("C:\\Users\\Kieran\\Downloads\\nh02_000_skin_01_mat.gmtl.gfxbin"); - // var material = reader.Read(); - // var json = JsonConvert.SerializeObject(material); - // File.WriteAllText("C:\\Testing\\NewSkinTemplate.json", json); - //BtexConverter.Convert("C:\\Testing\\Previews\\preview.png", - // "C:\\Testing\\Previews\\preview.btex", BtexConverter.TextureType.Color); + private static void Visit(string directory) + { + foreach (var file in Directory.EnumerateFiles(directory)) + { + if (!file.EndsWith(".earc") && !file.EndsWith(".heb") && !file.EndsWith(".hephysx") && + !file.EndsWith(".bk2") && !file.EndsWith(".sab")) + { + System.Console.WriteLine(file); + } + } + + foreach (var subdirectory in Directory.EnumerateDirectories(directory)) + { + Visit(subdirectory); + } } } \ No newline at end of file diff --git a/Flagrum.Console/Utilities/FileFinder.cs b/Flagrum.Console/Utilities/FileFinder.cs new file mode 100644 index 00000000..2001eec5 --- /dev/null +++ b/Flagrum.Console/Utilities/FileFinder.cs @@ -0,0 +1,123 @@ +using System.Collections.Concurrent; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Flagrum.Core.Archive; +using Newtonsoft.Json; + +namespace Flagrum.Console.Utilities; + +public class FileData +{ + public string Location { get; set; } + public string ArchiveLocation { get; set; } + public string Uri { get; set; } + public string FileName { get; set; } +} + +public class FileFinder +{ + private const string DataDirectory = @"C:\Program Files (x86)\Steam\steamapps\common\FINAL FANTASY XV\datas"; + + private readonly string _startDirectory; + private string _fileName; + private ConcurrentBag _map; + + public FileFinder(string startDirectory) + { + _startDirectory = startDirectory; + } + + public void GenerateMap() + { + System.Console.WriteLine("Started mapping..."); + var watch = Stopwatch.StartNew(); + + _map = new ConcurrentBag(); + MapDirectory(DataDirectory); + Parallel.ForEach(Directory.EnumerateDirectories(DataDirectory), GenerateMapRecursively); + + watch.Stop(); + System.Console.WriteLine($"Mapping finished after {watch.ElapsedMilliseconds} milliseconds."); + + File.WriteAllText(@"C:\Modding\map.json", JsonConvert.SerializeObject(_map)); + } + + private void GenerateMapRecursively(string directory) + { + MapDirectory(directory); + + foreach (var subdirectory in Directory.EnumerateDirectories(directory)) + { + GenerateMapRecursively(subdirectory); + } + } + + private void MapDirectory(string directory) + { + foreach (var file in Directory.EnumerateFiles(directory)) + { + _map.Add(new FileData + { + FileName = file.Split('\\').Last(), + Location = file + }); + + if (file.EndsWith(".earc")) + { + using var unpacker = new Unpacker(file); + foreach (var archiveFile in unpacker.Files.Where(f => !f.Flags.HasFlag(ArchiveFileFlag.Reference))) + { + _map.Add(new FileData + { + ArchiveLocation = file, + FileName = archiveFile.RelativePath.Split('\\', '/').Last(), + Location = file + "\\" + archiveFile.RelativePath, + Uri = archiveFile.Uri + }); + } + } + } + } + + public void FindByFileName(string fileName) + { + System.Console.WriteLine("Starting search..."); + var watch = Stopwatch.StartNew(); + + _fileName = fileName; + var startDirectory = $"{DataDirectory}\\{_startDirectory}"; + foreach (var directory in Directory.EnumerateDirectories(startDirectory)) + { + FindRecursively(directory); + } + + watch.Stop(); + System.Console.WriteLine($"Search finished after {watch.ElapsedMilliseconds} milliseconds."); + } + + private bool FindRecursively(string directory) + { + foreach (var file in Directory.EnumerateFiles(directory, "*.earc")) + { + using var unpacker = new Unpacker(file); + var result = unpacker.GetUriByQuery(_fileName); + if (result != null) + { + System.Console.WriteLine(result + " - " + file); + return true; + } + } + + foreach (var subdirectory in Directory.EnumerateDirectories(directory)) + { + if (FindRecursively(subdirectory)) + { + return true; + } + } + + return false; + } +} \ No newline at end of file diff --git a/Flagrum.Core/Archive/Unpacker.cs b/Flagrum.Core/Archive/Unpacker.cs index 79486641..ead5f9e5 100644 --- a/Flagrum.Core/Archive/Unpacker.cs +++ b/Flagrum.Core/Archive/Unpacker.cs @@ -1,10 +1,10 @@ using System; using System.Collections.Generic; using System.IO; -using System.IO.Compression; using System.Linq; using System.Text; using Flagrum.Core.Utilities; +using ZLibNet; namespace Flagrum.Core.Archive; @@ -23,6 +23,8 @@ public Unpacker(string archivePath) _header = ReadHeader(); } + public List Files => _files ??= ReadFileHeaders().ToList(); + public void Dispose() { _stream?.Dispose(); @@ -34,6 +36,12 @@ public bool HasFile(string uri) return _files.Any(f => f.Uri == uri); } + public string GetUriByQuery(string query) + { + _files ??= ReadFileHeaders().ToList(); + return _files.FirstOrDefault(f => f.Uri.EndsWith(query))?.Uri; + } + /// /// Retrieves the data for one file in the archive /// @@ -159,7 +167,6 @@ private byte[] Decompress(ArchiveFile file, byte[] data) } using var memoryStream = new MemoryStream(data); - using var zlibStream = new ZLibStream(memoryStream, CompressionMode.Decompress); using var outStream = new MemoryStream(); using var writer = new BinaryWriter(outStream); @@ -168,14 +175,13 @@ private byte[] Decompress(ArchiveFile file, byte[] data) // Align the bytes if (index > 0) { - var offset = 4 - (int)((file.DataOffset + (ulong)memoryStream.Position) % 4); - + var offset = 4 - (int)((_header.DataOffset + memoryStream.Position) % 4); if (offset > 3) { offset = 0; } - memoryStream.Seek(memoryStream.Position + offset, SeekOrigin.Begin); + memoryStream.Seek(offset, SeekOrigin.Current); } // Read the data sizes @@ -186,17 +192,14 @@ private byte[] Decompress(ArchiveFile file, byte[] data) memoryStream.Read(buffer, 0, 4); var decompressedSize = BitConverter.ToUInt32(buffer); - var readCount = 4096; - var position = 0; - - while (readCount > 0) - { - var size = (int)Math.Min(decompressedSize - position, 4096); - var decompressBuffer = new byte[size]; - readCount = zlibStream.Read(decompressBuffer, 0, size); - writer.Write(decompressBuffer, 0, readCount); - position += readCount; - } + // Decompress the current chunk and write to the output stream + buffer = new byte[compressedSize]; + memoryStream.Read(buffer, 0, (int)compressedSize); + using var stream = new MemoryStream(buffer); + using var zlibStream = new ZLibStream(stream, CompressionMode.Decompress); + var decompressedBuffer = new byte[decompressedSize]; + zlibStream.Read(decompressedBuffer, 0, (int)decompressedSize); + writer.Write(decompressedBuffer); } return outStream.ToArray(); @@ -272,6 +275,12 @@ private ArchiveHeader ReadHeader() return header; } + public static byte[] GetFileByLocation(string earcLocation, string fileQuery) + { + using var unpacker = new Unpacker(earcLocation); + return unpacker.UnpackFileByQuery(fileQuery, out _); + } + private byte ReadByte() { var buffer = new byte[1]; diff --git a/Flagrum.Core/Archive/ZLibNet/Helpers.cs b/Flagrum.Core/Archive/ZLibNet/Helpers.cs new file mode 100644 index 00000000..26d15537 --- /dev/null +++ b/Flagrum.Core/Archive/ZLibNet/Helpers.cs @@ -0,0 +1,161 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Collections; +using System.Runtime.InteropServices; +using System.IO; +using System.Reflection; + +namespace ZLibNet +{ + internal class FixedArray : IDisposable + { + GCHandle pHandle; + Array pArray; + + public FixedArray(Array array) + { + pArray = array; + pHandle = GCHandle.Alloc(array, GCHandleType.Pinned); + } + + ~FixedArray() + { + pHandle.Free(); + } + + #region IDisposable Members + + public void Dispose() + { + pHandle.Free(); + GC.SuppressFinalize(this); + } + + public IntPtr this[int idx] + { + get + { + return Marshal.UnsafeAddrOfPinnedArrayElement(pArray, idx); + } + } + public static implicit operator IntPtr(FixedArray fixedArray) + { + return fixedArray[0]; + } + #endregion + } + + public static class ListHelper + { + public static void Add(this List list, params T[] items) + { + foreach (T i in items) + list.Add(i); + } + public static void AddRange(this List list, IEnumerable items) + { + foreach (T i in items) + list.Add(i); + } + } + + + internal static class BitFlag + { + internal static bool IsSet(int bits, int flag) + { + return (bits & flag) == flag; + } + internal static bool IsSet(uint bits, uint flag) + { + return (bits & flag) == flag; + } + //internal static uint Set(uint bits, uint flag) + //{ + // return bits | flag; + //} + //internal static int Set(int bits, int flag) + //{ + // return bits | flag; + //} + } + + public static class DllLoader + { + + [DllImport("kernel32", SetLastError = true, CharSet = CharSet.Unicode)] + static extern IntPtr LoadLibrary(string lpFileName); + + // http://stackoverflow.com/questions/666799/embedding-unmanaged-dll-into-a-managed-c-sharp-dll + public static void Load() + { + var thisAss = Assembly.GetExecutingAssembly(); + + // Get a temporary directory in which we can store the unmanaged DLL, with + // this assembly's version number in the path in order to avoid version + // conflicts in case two applications are running at once with different versions + string dirName = Path.Combine(Path.GetTempPath(), "zlibnet-zlib" + ZLibDll.ZLibDllFileVersion); + + try + { + if (!Directory.Exists(dirName)) + Directory.CreateDirectory(dirName); + } + catch + { + // raced? + if (!Directory.Exists(dirName)) + throw; + } + + string dllName = ZLibDll.GetDllName(); + string dllFullName = Path.Combine(dirName, dllName); + + // Get the embedded resource stream that holds the Internal DLL in this assembly. + // The name looks funny because it must be the default namespace of this project + // (MyAssembly.) plus the name of the Properties subdirectory where the + // embedded resource resides (Properties.) plus the name of the file. + if (!File.Exists(dllFullName)) + { + // Copy the assembly to the temporary file + string tempFile = Path.GetTempFileName(); + using (Stream stm = thisAss.GetManifestResourceStream("ZLibNet." + dllName)) + { + using (Stream outFile = File.Create(tempFile)) + { + stm.CopyTo(outFile); + } + } + + try + { + File.Move(tempFile, dllFullName); + } + catch + { + // clean up tempfile + try + { + File.Delete(tempFile); + } + catch + { + // eat + } + + // raced? + if (!File.Exists(dllFullName)) + throw; + } + + } + + // We must explicitly load the DLL here because the temporary directory is not in the PATH. + // Once it is loaded, the DllImport directives that use the DLL will use the one that is already loaded into the process. + IntPtr hFile = LoadLibrary(dllFullName); + if (hFile == IntPtr.Zero) + throw new Exception("Can't load " + dllFullName); + } + } +} diff --git a/Flagrum.Core/Archive/ZLibNet/Minizip.cs b/Flagrum.Core/Archive/ZLibNet/Minizip.cs new file mode 100644 index 00000000..31b30c83 --- /dev/null +++ b/Flagrum.Core/Archive/ZLibNet/Minizip.cs @@ -0,0 +1,698 @@ +using System; +using System.IO; +using System.Text; +using System.Runtime.InteropServices; +using System.Collections.Generic; +using System.Collections; +using System.Runtime.Serialization; + +namespace ZLibNet +{ + + /// Support methods for uncompressing zip files. + /// + /// This unzip package allow extract file from .ZIP file, compatible with PKZip 2.04g WinZip, InfoZip tools and compatible. + /// Encryption and multi volume ZipFile (span) are not supported. Old compressions used by old PKZip 1.x are not supported. + /// Copyright (C) 1998 Gilles Vollant. http://www.winimage.com/zLibDll/unzip.htm + /// C# wrapper by Gerry Shaw (gerry_shaw@yahoo.com). http://www.organicbit.com/zip/ + /// + /// ZipLib = MiniZip part of zlib + /// + /// + internal static class Minizip + { + [DllImport(ZLibDll.Name32, EntryPoint = "setOpenUnicode", ExactSpelling = true)] + static extern int setOpenUnicode_32(int openUnicode); + [DllImport(ZLibDll.Name64, EntryPoint = "setOpenUnicode", ExactSpelling = true)] + static extern int setOpenUnicode_64(int openUnicode); + + internal static bool setOpenUnicode(bool openUnicode) + { + int oldVal; + if (ZLibDll.Is64) + oldVal = setOpenUnicode_64(openUnicode ? 1 : 0); + else + oldVal = setOpenUnicode_32(openUnicode ? 1 : 0); + return oldVal == 1; + } + + static Minizip() + { + DllLoader.Load(); + } + + /* + Create a zipfile. + pathname contain on Windows NT a filename like "c:\\zlib\\zlib111.zip" or on an Unix computer "zlib/zlib111.zip". + if the file pathname exist and append=1, the zip will be created at the end of the file. (useful if the file contain a self extractor code) + If the zipfile cannot be opened, the return value is NULL. + Else, the return value is a zipFile Handle, usable with other function of this zip package. + */ + /// Create a zip file. + [DllImport(ZLibDll.Name32, EntryPoint = "zipOpen64", ExactSpelling = true, CharSet = CharSet.Unicode)] + static extern IntPtr zipOpen_32(string fileName, int append); + [DllImport(ZLibDll.Name64, EntryPoint = "zipOpen64", ExactSpelling = true, CharSet = CharSet.Unicode)] + static extern IntPtr zipOpen_64(string fileName, int append); + + internal static IntPtr zipOpen(string fileName, bool append) + { + setOpenUnicode(true); + + if (ZLibDll.Is64) + return zipOpen_64(fileName, append ? 1 : 0); + else + return zipOpen_32(fileName, append ? 1 : 0); + } + /* + Open a file in the ZIP for writing. + filename : the filename in zip (if NULL, '-' without quote will be used + *zipfi contain supplemental information + if extrafield_local!=NULL and size_extrafield_local>0, extrafield_local contains the extrafield data the the local header + if extrafield_global!=NULL and size_extrafield_global>0, extrafield_global contains the extrafield data the the local header + if comment != NULL, comment contain the comment string + method contain the compression method (0 for store, Z_DEFLATED for deflate) + level contain the level of compression (can be Z_DEFAULT_COMPRESSION) + */ + [DllImport(ZLibDll.Name32, EntryPoint = "zipOpenNewFileInZip4_64", ExactSpelling = true)] + static extern int zipOpenNewFileInZip4_64_32(IntPtr handle, + byte[] entryName, + ref ZipFileEntryInfo entryInfoPtr, + byte[] extraField, + uint extraFieldLength, + byte[] extraFieldGlobal, + uint extraFieldGlobalLength, + byte[] comment, + int method, + int level, + int raw, + int windowBits, + int memLevel, + int strategy, + byte[] password, + uint crcForCrypting, + uint versionMadeBy, + uint flagBase, + int zip64); + [DllImport(ZLibDll.Name64, EntryPoint = "zipOpenNewFileInZip4_64", ExactSpelling = true)] + static extern int zipOpenNewFileInZip4_64_64(IntPtr handle, + byte[] entryName, + ref ZipFileEntryInfo entryInfoPtr, + byte[] extraField, + uint extraFieldLength, + byte[] extraFieldGlobal, + uint extraFieldGlobalLength, + byte[] comment, + int method, + int level, + int raw, + int windowBits, + int memLevel, + int strategy, + byte[] password, + uint crcForCrypting, + uint versionMadeBy, + uint flagBase, + int zip64); + + + public static int zipOpenNewFileInZip4_64(IntPtr handle, + byte[] entryName, + ref ZipFileEntryInfo entryInfoPtr, + byte[] extraField, + uint extraFieldLength, + byte[] extraFieldGlobal, + uint extraFieldGlobalLength, + byte[] comment, + int method, + int level, + uint flagBase, + bool zip64 + ) + { + if (ZLibDll.Is64) + return zipOpenNewFileInZip4_64_64(handle, entryName, ref entryInfoPtr, extraField, extraFieldLength, + extraFieldGlobal, extraFieldGlobalLength, comment, method, level, 0, -ZLib.MAX_WBITS, + ZLib.DEF_MEM_LEVEL, ZLib.Z_DEFAULT_STRATEGY, + null, 0, ZLib.VERSIONMADEBY, flagBase, zip64 ? 1 : 0); + else + return zipOpenNewFileInZip4_64_32(handle, entryName, ref entryInfoPtr, extraField, extraFieldLength, + extraFieldGlobal, extraFieldGlobalLength, comment, method, level, 0, -ZLib.MAX_WBITS, + ZLib.DEF_MEM_LEVEL, ZLib.Z_DEFAULT_STRATEGY, + null, 0, ZLib.VERSIONMADEBY, flagBase, zip64 ? 1 : 0); + } + + + + /// Write data to the zip file. + [DllImport(ZLibDll.Name32, EntryPoint = "zipWriteInFileInZip", ExactSpelling = true)] + static extern int zipWriteInFileInZip_32(IntPtr handle, IntPtr buffer, uint count); + [DllImport(ZLibDll.Name64, EntryPoint = "zipWriteInFileInZip", ExactSpelling = true)] + static extern int zipWriteInFileInZip_64(IntPtr handle, IntPtr buffer, uint count); + + internal static int zipWriteInFileInZip(IntPtr handle, IntPtr buffer, uint count) + { + if (ZLibDll.Is64) + return zipWriteInFileInZip_64(handle, buffer, count); + else + return zipWriteInFileInZip_32(handle, buffer, count); + } + + /// Close the current entry in the zip file. + [DllImport(ZLibDll.Name32, EntryPoint = "zipCloseFileInZip", ExactSpelling = true)] + static extern int zipCloseFileInZip_32(IntPtr handle); + [DllImport(ZLibDll.Name64, EntryPoint = "zipCloseFileInZip", ExactSpelling = true)] + static extern int zipCloseFileInZip_64(IntPtr handle); + + internal static int zipCloseFileInZip(IntPtr handle) + { + if (ZLibDll.Is64) + return zipCloseFileInZip_64(handle); + else + return zipCloseFileInZip_32(handle); + } + + /// Close the zip file. + /// //file comment is for some weird reason ANSI, while entry name + comment is OEM... + [DllImport(ZLibDll.Name32, EntryPoint = "zipClose", ExactSpelling = true, CharSet = CharSet.Ansi)] + static extern int zipClose_32(IntPtr handle, string comment); + [DllImport(ZLibDll.Name64, EntryPoint = "zipClose", ExactSpelling = true, CharSet = CharSet.Ansi)] + static extern int zipClose_64(IntPtr handle, string comment); + + internal static int zipClose(IntPtr handle, string comment) + { + if (ZLibDll.Is64) + return zipClose_64(handle, comment); + else + return zipClose_32(handle, comment); + } + + /// Opens a zip file for reading. + /// The name of the zip to open. + /// + /// A handle usable with other functions of the ZipLib class. + /// Otherwise IntPtr.Zero if the zip file could not e opened (file doen not exist or is not valid). + /// + [DllImport(ZLibDll.Name32, EntryPoint = "unzOpen64", ExactSpelling = true, CharSet = CharSet.Unicode)] + static extern IntPtr unzOpen_32(string fileName); + [DllImport(ZLibDll.Name64, EntryPoint = "unzOpen64", ExactSpelling = true, CharSet = CharSet.Unicode)] + static extern IntPtr unzOpen_64(string fileName); + + internal static IntPtr unzOpen(string fileName) + { + setOpenUnicode(true); + + if (ZLibDll.Is64) + return unzOpen_64(fileName); + else + return unzOpen_32(fileName); + } + + /// Closes a zip file opened with unzipOpen. + /// The zip file handle opened by . + /// If there are files inside the zip file opened with these files must be closed with before call unzClose. + /// + /// Zero if there was no error. + /// Otherwise a value less than zero. See for the specific reason. + /// + [DllImport(ZLibDll.Name32, EntryPoint = "unzClose", ExactSpelling = true)] + static extern int unzClose_32(IntPtr handle); + [DllImport(ZLibDll.Name64, EntryPoint = "unzClose", ExactSpelling = true)] + static extern int unzClose_64(IntPtr handle); + + internal static int unzClose(IntPtr handle) + { + if (ZLibDll.Is64) + return unzClose_64(handle); + else + return unzClose_32(handle); + } + + /// Get global information about the zip file. + /// The zip file handle opened by . + /// An address of a struct to hold the information. No preparation of the structure is needed. + /// + /// Zero if there was no error. + /// Otherwise a value less than zero. See for the specific reason. + /// + [DllImport(ZLibDll.Name32, EntryPoint = "unzGetGlobalInfo", ExactSpelling = true)] + static extern int unzGetGlobalInfo_32(IntPtr handle, out ZipFileInfo globalInfoPtr); + [DllImport(ZLibDll.Name64, EntryPoint = "unzGetGlobalInfo", ExactSpelling = true)] + static extern int unzGetGlobalInfo_64(IntPtr handle, out ZipFileInfo globalInfoPtr); + + internal static int unzGetGlobalInfo(IntPtr handle, out ZipFileInfo globalInfoPtr) + { + if (ZLibDll.Is64) + return unzGetGlobalInfo_64(handle, out globalInfoPtr); + else + return unzGetGlobalInfo_32(handle, out globalInfoPtr); + } + + /// Get the comment associated with the entire zip file. + /// The zip file handle opened by + /// The buffer to hold the comment. + /// The length of the buffer in bytes (8 bit characters). + /// + /// The number of characters in the comment if there was no error. + /// Otherwise a value less than zero. See for the specific reason. + /// + [DllImport(ZLibDll.Name32, EntryPoint = "unzGetGlobalComment", ExactSpelling = true)] + static extern int unzGetGlobalComment_32(IntPtr handle, byte[] commentBuffer, uint commentBufferLength); + [DllImport(ZLibDll.Name64, EntryPoint = "unzGetGlobalComment", ExactSpelling = true)] + static extern int unzGetGlobalComment_64(IntPtr handle, byte[] commentBuffer, uint commentBufferLength); + + internal static int unzGetGlobalComment(IntPtr handle, byte[] commentBuffer, uint commentBufferLength) + { + if (ZLibDll.Is64) + return unzGetGlobalComment_64(handle, commentBuffer, commentBufferLength); + else + return unzGetGlobalComment_32(handle, commentBuffer, commentBufferLength); + } + + /// Set the current file of the zip file to the first file. + /// The zip file handle opened by . + /// + /// Zero if there was no error. + /// Otherwise a value less than zero. See for the specific reason. + /// + [DllImport(ZLibDll.Name32, EntryPoint = "unzGoToFirstFile", ExactSpelling = true)] + static extern int unzGoToFirstFile_32(IntPtr handle); + [DllImport(ZLibDll.Name64, EntryPoint = "unzGoToFirstFile", ExactSpelling = true)] + static extern int unzGoToFirstFile_64(IntPtr handle); + + internal static int unzGoToFirstFile(IntPtr handle) + { + if (ZLibDll.Is64) + return unzGoToFirstFile_64(handle); + else + return unzGoToFirstFile_32(handle); + } + + /// Set the current file of the zip file to the next file. + /// The zip file handle opened by . + /// + /// Zero if there was no error. + /// Otherwise if there are no more entries. + /// + [DllImport(ZLibDll.Name32, EntryPoint = "unzGoToNextFile", ExactSpelling = true)] + static extern int unzGoToNextFile_32(IntPtr handle); + [DllImport(ZLibDll.Name64, EntryPoint = "unzGoToNextFile", ExactSpelling = true)] + static extern int unzGoToNextFile_64(IntPtr handle); + + internal static int unzGoToNextFile(IntPtr handle) + { + if (ZLibDll.Is64) + return unzGoToNextFile_64(handle); + else + return unzGoToNextFile_32(handle); + } + + /// Try locate the entry in the zip file. + /// The zip file handle opened by . + /// The name of the entry to look for. + /// If 0 use the OS default. If 1 use case sensitivity like strcmp, Unix style. If 2 do not use case sensitivity like strcmpi, Windows style. + /// + /// Zero if there was no error. + /// Otherwise if there are no more entries. + /// + //[DllImport(ZLibDll.Name, ExactSpelling = true, CharSet = CharSet.Ansi)] + //public static extern int unzLocateFile(IntPtr handle, string entryName, int caseSensitivity); + + /// Get information about the current entry in the zip file. + /// The zip file handle opened by . + /// A ZipEntryInfo struct to hold information about the entry or null. + /// An array of sbyte characters to hold the entry name or null. + /// The length of the entryNameBuffer in bytes. + /// An array to hold the extra field data for the entry or null. + /// The length of the extraField array in bytes. + /// An array of sbyte characters to hold the entry name or null. + /// The length of theh commentBuffer in bytes. + /// + /// If entryInfoPtr is not null the structure will contain information about the current file. + /// If entryNameBuffer is not null the name of the entry will be copied into it. + /// If extraField is not null the extra field data of the entry will be copied into it. + /// If commentBuffer is not null the comment of the entry will be copied into it. + /// + /// + /// Zero if there was no error. + /// Otherwise a value less than zero. See for the specific reason. + /// + [DllImport(ZLibDll.Name32, EntryPoint = "unzGetCurrentFileInfo64", ExactSpelling = true)] + static extern int unzGetCurrentFileInfo64_32( + IntPtr handle, + out ZipEntryInfo64 entryInfoPtr, + byte[] entryNameBuffer, + uint entryNameBufferLength, + byte[] extraField, + uint extraFieldLength, + byte[] commentBuffer, + uint commentBufferLength); + [DllImport(ZLibDll.Name64, EntryPoint = "unzGetCurrentFileInfo64", ExactSpelling = true)] + static extern int unzGetCurrentFileInfo64_64( + IntPtr handle, + out ZipEntryInfo64 entryInfoPtr, + byte[] entryNameBuffer, + uint entryNameBufferLength, + byte[] extraField, + uint extraFieldLength, + byte[] commentBuffer, + uint commentBufferLength); + + static internal int unzGetCurrentFileInfo64( + IntPtr handle, + out ZipEntryInfo64 entryInfoPtr, + byte[] entryNameBuffer, + uint entryNameBufferLength, + byte[] extraField, + uint extraFieldLength, + byte[] commentBuffer, + uint commentBufferLength) + { + if (ZLibDll.Is64) + return unzGetCurrentFileInfo64_64(handle, out entryInfoPtr, entryNameBuffer, entryNameBufferLength, extraField, extraFieldLength, + commentBuffer, commentBufferLength); + else + return unzGetCurrentFileInfo64_32(handle, out entryInfoPtr, entryNameBuffer, entryNameBufferLength, extraField, extraFieldLength, + commentBuffer, commentBufferLength); + + } + + [DllImport("kernel32.dll")] + public static extern uint GetOEMCP(); + + public static Encoding OEMEncoding = Encoding.GetEncoding((int)Minizip.GetOEMCP()); + + /// Open the zip file entry for reading. + /// The zip file handle opened by . + /// + /// Zero if there was no error. + /// Otherwise a value from . + /// + [DllImport(ZLibDll.Name32, EntryPoint = "unzOpenCurrentFile", ExactSpelling = true)] + public static extern int unzOpenCurrentFile_32(IntPtr handle); + [DllImport(ZLibDll.Name64, EntryPoint = "unzOpenCurrentFile", ExactSpelling = true)] + public static extern int unzOpenCurrentFile_64(IntPtr handle); + + internal static int unzOpenCurrentFile(IntPtr handle) + { + if (ZLibDll.Is64) + return unzOpenCurrentFile_64(handle); + else + return unzOpenCurrentFile_32(handle); + } + + /// Close the file entry opened by . + /// The zip file handle opened by . + /// + /// Zero if there was no error. + /// CrcError if the file was read but the Crc does not match. + /// Otherwise a value from . + /// + [DllImport(ZLibDll.Name32, EntryPoint = "unzCloseCurrentFile", ExactSpelling = true)] + public static extern int unzCloseCurrentFile_32(IntPtr handle); + [DllImport(ZLibDll.Name64, EntryPoint = "unzCloseCurrentFile", ExactSpelling = true)] + public static extern int unzCloseCurrentFile_64(IntPtr handle); + + internal static int unzCloseCurrentFile(IntPtr handle) + { + if (ZLibDll.Is64) + return unzCloseCurrentFile_64(handle); + else + return unzCloseCurrentFile_32(handle); + } + + /// Read bytes from the current zip file entry. + /// The zip file handle opened by . + /// Buffer to store the uncompressed data into. + /// Number of bytes to write from . + /// + /// The number of byte copied if somes bytes are copied. + /// Zero if the end of file was reached. + /// Less than zero with error code if there is an error. See for a list of possible error codes. + /// + [DllImport(ZLibDll.Name32, EntryPoint = "unzReadCurrentFile", ExactSpelling = true)] + static extern int unzReadCurrentFile_32(IntPtr handle, IntPtr buffer, uint count); + [DllImport(ZLibDll.Name64, EntryPoint = "unzReadCurrentFile", ExactSpelling = true)] + static extern int unzReadCurrentFile_64(IntPtr handle, IntPtr buffer, uint count); + + internal static int unzReadCurrentFile(IntPtr handle, IntPtr buffer, uint count) + { + if (ZLibDll.Is64) + return unzReadCurrentFile_64(handle, buffer, count); + else + return unzReadCurrentFile_32(handle, buffer, count); + } + + /// Give the current position in uncompressed data of the zip file entry currently opened. + /// The zip file handle opened by . + /// The number of bytes into the uncompressed data read so far. + //[DllImport(ZLibDll.Name)] + //public static extern long unztell(IntPtr handle); + + /// Determine if the end of the zip file entry has been reached. + /// The zip file handle opened by . + /// + /// One if the end of file was reached. + /// Zero if elsewhere. + /// + //[DllImport(ZLibDll.Name)] + //public static extern int unzeof(IntPtr handle); + + } + + internal static class ZipEntryFlag + { + internal const uint UTF8 = 0x800; //1 << 11 + } + + /// Global information about the zip file. + [StructLayout(LayoutKind.Sequential)] + internal struct ZipFileInfo + { + /// The number of entries in the directory. + public UInt32 EntryCount; + + /// Length of zip file comment in bytes (8 bit characters). + public UInt32 CommentLength; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct ZipFileEntryInfo + { + public ZipDateTimeInfo ZipDateTime; + public UInt32 DosDate; + public UInt32 InternalFileAttributes; // 2 bytes + public UInt32 ExternalFileAttributes; // 4 bytes + } + + /// Custom ZipLib date time structure. + [StructLayout(LayoutKind.Sequential)] + internal struct ZipDateTimeInfo + { + /// Seconds after the minute - [0,59] + public UInt32 Seconds; + + /// Minutes after the hour - [0,59] + public UInt32 Minutes; + + /// Hours since midnight - [0,23] + public UInt32 Hours; + + /// Day of the month - [1,31] + public UInt32 Day; + + /// Months since January - [0,11] + public UInt32 Month; + + /// Years - [1980..2044] + public UInt32 Year; + + // implicit conversion from DateTime to ZipDateTimeInfo + public static implicit operator ZipDateTimeInfo(DateTime date) + { + ZipDateTimeInfo d; + d.Seconds = (uint)date.Second; + d.Minutes = (uint)date.Minute; + d.Hours = (uint)date.Hour; + d.Day = (uint)date.Day; + d.Month = (uint)date.Month - 1; + d.Year = (uint)date.Year; + return d; + } + + public static implicit operator DateTime(ZipDateTimeInfo date) + { + DateTime dt = new DateTime( + (int)date.Year, + (int)date.Month + 1, + (int)date.Day, + (int)date.Hours, + (int)date.Minutes, + (int)date.Seconds); + return dt; + } + + } + + /// Information stored in zip file directory about an entry. + [StructLayout(LayoutKind.Sequential)] + internal struct ZipEntryInfo64 + { + // Version made by (2 bytes). + public UInt32 Version; + + /// Version needed to extract (2 bytes). + public UInt32 VersionNeeded; + + /// General purpose bit flag (2 bytes). + public UInt32 Flag; + + /// Compression method (2 bytes). + public UInt32 CompressionMethod; + + /// Last mod file date in Dos fmt (4 bytes). + public UInt32 DosDate; + + /// Crc-32 (4 bytes). + public UInt32 Crc; + + /// Compressed size (8 bytes). + public UInt64 CompressedSize; + + /// Uncompressed size (8 bytes). + public UInt64 UncompressedSize; + + /// Filename length (2 bytes). + public UInt32 FileNameLength; + + /// Extra field length (2 bytes). + public UInt32 ExtraFieldLength; + + /// File comment length (2 bytes). + public UInt32 CommentLength; + + /// Disk number start (2 bytes). + public UInt32 DiskStartNumber; + + /// Internal file attributes (2 bytes). + public UInt32 InternalFileAttributes; + + /// External file attributes (4 bytes). + public UInt32 ExternalFileAttributes; + + /// File modification date of entry. + public ZipDateTimeInfo ZipDateTime; + } + + + /// Specifies how the the zip entry should be compressed. + public enum CompressionMethod + { + /// No compression. + Stored = 0, + + /// Default and only supported compression method. + Deflated = 8 + } + + /// Type of compression to use for the GZipStream. Currently only Decompress is supported. + public enum CompressionMode + { + /// Compresses the underlying stream. + Compress, + /// Decompresses the underlying stream. + Decompress, + } + + /// List of possible error codes. + /// + /// + internal static class ZipReturnCode + { + /// No error. + internal const int Ok = 0; + + /// Unknown error. + internal const int Error = -1; + + /// Last entry in directory reached. + internal const int EndOfListOfFile = -100; + + /// Parameter error. + internal const int ParameterError = -102; + + /// Zip file is invalid. + internal const int BadZipFile = -103; + + /// Internal program error. + internal const int InternalError = -104; + + /// Crc values do not match. + internal const int CrcError = -105; + + public static string GetMessage(int retCode) + { + switch (retCode) + { + case ZipReturnCode.Ok: + return "No error"; + case ZipReturnCode.Error: + return "Unknown error"; + case ZipReturnCode.EndOfListOfFile: + return "Last entry in directory reached"; + case ZipReturnCode.ParameterError: + return "Parameter error"; + case ZipReturnCode.BadZipFile: + return "Zip file is invalid"; + case ZipReturnCode.InternalError: + return "Internal program error"; + case ZipReturnCode.CrcError: + return "Crc values do not match"; + default: + return "Unknown error: " + retCode; + } + } + } + + + /// Thrown whenever an error occurs during the build. + [Serializable] + public class ZipException : ApplicationException + { + + /// Constructs an exception with no descriptive information. + public ZipException() + : base() + { + } + + /// Constructs an exception with a descriptive message. + /// The error message that explains the reason for the exception. + public ZipException(String message) + : base(message) + { + } + + public ZipException(String message, int errorCode) + : base(message + " (" + ZipReturnCode.GetMessage(errorCode) + ")") + { + } + + /// Constructs an exception with a descriptive message and a reference to the instance of the Exception that is the root cause of the this exception. + /// The error message that explains the reason for the exception. + /// An instance of Exception that is the cause of the current Exception. If is non-null, then the current Exception is raised in a catch block handling innerException. + public ZipException(String message, Exception innerException) + : base(message, innerException) + { + } + + /// Initializes a new instance of the BuildException class with serialized data. + /// The object that holds the serialized object data. + /// The contextual information about the source or destination. + public ZipException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + } + + +} diff --git a/Flagrum.Core/Archive/ZLibNet/ZLib.cs b/Flagrum.Core/Archive/ZLibNet/ZLib.cs new file mode 100644 index 00000000..26ae37b7 --- /dev/null +++ b/Flagrum.Core/Archive/ZLibNet/ZLib.cs @@ -0,0 +1,295 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Runtime.InteropServices; +using System.Runtime.Serialization; +using System.IO; +using System.Reflection; + +namespace ZLibNet +{ + internal static class ZLib + { + internal const string ZLibVersion = "1.2.8"; + internal const int MAX_WBITS = 15; /* 32K LZ77 window */ + internal const int DEF_MEM_LEVEL = 8; + internal const int Z_DEFAULT_STRATEGY = 0; + internal const uint VERSIONMADEBY = 0; + + const int Z_DEFLATED = 8; + /* The deflate compression method (the only one supported in this version) */ + + static ZLib() + { + DllLoader.Load(); + } + + + [DllImport(ZLibDll.Name32, EntryPoint = "inflateInit2_", ExactSpelling = true, CharSet = CharSet.Ansi)] + static extern int inflateInit2_32(ref z_stream strm, int windowBits, string version, int stream_size); + [DllImport(ZLibDll.Name64, EntryPoint = "inflateInit2_", ExactSpelling = true, CharSet = CharSet.Ansi)] + static extern int inflateInit2_64(ref z_stream strm, int windowBits, string version, int stream_size); + + internal static int inflateInit(ref z_stream strm, ZLibOpenType windowBits) + { + if (ZLibDll.Is64) + return inflateInit2_64(ref strm, (int)windowBits, ZLib.ZLibVersion, Marshal.SizeOf(typeof(z_stream))); + else + return inflateInit2_32(ref strm, (int)windowBits, ZLib.ZLibVersion, Marshal.SizeOf(typeof(z_stream))); + } + + [DllImport(ZLibDll.Name32, EntryPoint = "deflateInit2_", ExactSpelling = true, CharSet = CharSet.Ansi)] + static extern int deflateInit2_32(ref z_stream strm, int level, int method, int windowBits, + int memLevel, int strategy, string version, int stream_size); + [DllImport(ZLibDll.Name64, EntryPoint = "deflateInit2_", ExactSpelling = true, CharSet = CharSet.Ansi)] + static extern int deflateInit2_64(ref z_stream strm, int level, int method, int windowBits, + int memLevel, int strategy, string version, int stream_size); + + internal static int deflateInit(ref z_stream strm, CompressionLevel level, ZLibWriteType windowBits) + { + if (ZLibDll.Is64) + return deflateInit2_64(ref strm, (int)level, Z_DEFLATED, (int)windowBits, DEF_MEM_LEVEL, + Z_DEFAULT_STRATEGY, ZLibVersion, Marshal.SizeOf(typeof(z_stream))); + else + return deflateInit2_32(ref strm, (int)level, Z_DEFLATED, (int)windowBits, DEF_MEM_LEVEL, + Z_DEFAULT_STRATEGY, ZLibVersion, Marshal.SizeOf(typeof(z_stream))); + } + + [DllImport(ZLibDll.Name32, EntryPoint = "inflate", ExactSpelling = true)] + static extern int inflate_32(ref z_stream strm, ZLibFlush flush); + [DllImport(ZLibDll.Name64, EntryPoint = "inflate", ExactSpelling = true)] + static extern int inflate_64(ref z_stream strm, ZLibFlush flush); + + internal static int inflate(ref z_stream strm, ZLibFlush flush) + { + if (ZLibDll.Is64) + return inflate_64(ref strm, flush); + else + return inflate_32(ref strm, flush); + } + + [DllImport(ZLibDll.Name32, EntryPoint = "deflate", ExactSpelling = true)] + static extern int deflate_32(ref z_stream strm, ZLibFlush flush); + [DllImport(ZLibDll.Name64, EntryPoint = "deflate", ExactSpelling = true)] + static extern int deflate_64(ref z_stream strm, ZLibFlush flush); + + internal static int deflate(ref z_stream strm, ZLibFlush flush) + { + if (ZLibDll.Is64) + return deflate_64(ref strm, flush); + else + return deflate_32(ref strm, flush); + } + + [DllImport(ZLibDll.Name32, EntryPoint = "inflateEnd", ExactSpelling = true)] + static extern int inflateEnd_32(ref z_stream strm); + [DllImport(ZLibDll.Name64, EntryPoint = "inflateEnd", ExactSpelling = true)] + static extern int inflateEnd_64(ref z_stream strm); + + internal static int inflateEnd(ref z_stream strm) + { + if (ZLibDll.Is64) + return inflateEnd_64(ref strm); + else + return inflateEnd_32(ref strm); + } + + [DllImport(ZLibDll.Name32, EntryPoint = "deflateEnd", ExactSpelling = true)] + static extern int deflateEnd_32(ref z_stream strm); + [DllImport(ZLibDll.Name64, EntryPoint = "deflateEnd", ExactSpelling = true)] + static extern int deflateEnd_64(ref z_stream strm); + + internal static int deflateEnd(ref z_stream strm) + { + if (ZLibDll.Is64) + return deflateEnd_64(ref strm); + else + return deflateEnd_32(ref strm); + } + + [DllImport(ZLibDll.Name32, EntryPoint = "crc32", ExactSpelling = true)] + static extern uint crc32_32(uint crc, IntPtr buffer, uint len); + [DllImport(ZLibDll.Name64, EntryPoint = "crc32", ExactSpelling = true)] + static extern uint crc32_64(uint crc, IntPtr buffer, uint len); + + internal static uint crc32(uint crc, IntPtr buffer, uint len) + { + if (ZLibDll.Is64) + return crc32_64(crc, buffer, len); + else + return crc32_32(crc, buffer, len); + } + } + + enum ZLibFlush + { + NoFlush = 0, //Z_NO_FLUSH + PartialFlush = 1, + SyncFlush = 2, + FullFlush = 3, + Finish = 4 // Z_FINISH + } + + enum ZLibCompressionStrategy + { + Filtered = 1, + HuffmanOnly = 2, + DefaultStrategy = 0 + } + + //enum ZLibCompressionMethod + //{ + // Delated = 8 + //} + + enum ZLibDataType + { + Binary = 0, + Ascii = 1, + Unknown = 2, + } + + public enum ZLibOpenType + { + //If a compressed stream with a larger window + //size is given as input, inflate() will return with the error code + //Z_DATA_ERROR instead of trying to allocate a larger window. + Deflate = -15, // -8..-15 + ZLib = 15, // 8..15, 0 = use the window size in the zlib header of the compressed stream. + GZip = 15 + 16, + Both_ZLib_GZip = 15 + 32, + } + + public enum ZLibWriteType + { + //If a compressed stream with a larger window + //size is given as input, inflate() will return with the error code + //Z_DATA_ERROR instead of trying to allocate a larger window. + Deflate = -15, // -8..-15 + ZLib = 15, // 8..15, 0 = use the window size in the zlib header of the compressed stream. + GZip = 15 + 16, + // Both = 15 + 32, + } + + public enum CompressionLevel + { + NoCompression = 0, + BestSpeed = 1, + BestCompression = 9, + // The "real" default is -1. Currently, zlib interpret -1 as 6, but they are free to change the interpretation. + // The reason for overriding the default and using 5 is I want this library to match DynaZip's default + // compression ratio and speed, and 5 was the best match (6 was somewhat slower than dynazip default). + Default = 5, + Level0 = 0, + Level1 = 1, + Level2 = 2, + Level3 = 3, + Level4 = 4, + Level5 = 5, + Level6 = 6, + Level7 = 7, + Level8 = 8, + Level9 = 9 + } + + [StructLayout(LayoutKind.Sequential)] + struct z_stream + { + public IntPtr next_in; /* next input byte */ + public uint avail_in; /* number of bytes available at next_in */ + public uint total_in; /* total nb of input bytes read so far */ + + public IntPtr next_out; /* next output byte should be put there */ + public uint avail_out; /* remaining free space at next_out */ + public uint total_out; /* total nb of bytes output so far */ + + private IntPtr msg; /* last error message, NULL if no error */ + + private IntPtr state; /* not visible by applications */ + + private IntPtr zalloc; /* used to allocate the internal state */ + private IntPtr zfree; /* used to free the internal state */ + private IntPtr opaque; /* private data object passed to zalloc and zfree */ + + public ZLibDataType data_type; /* best guess about the data type: ascii or binary */ + public uint adler; /* adler32 value of the uncompressed data */ + private uint reserved; /* reserved for future use */ + + public string lasterrormsg + { + get + { + return Marshal.PtrToStringAnsi(msg); + } + } + } + + internal static class ZLibReturnCode + { + public const int Ok = 0; + public const int StreamEnd = 1; //positive = no error + public const int NeedDictionary = 2; //positive = no error? + public const int Errno = -1; + public const int StreamError = -2; + public const int DataError = -3; //CRC + public const int MemoryError = -4; + public const int BufferError = -5; + public const int VersionError = -6; + + public static string GetMesage(int retCode) + { + switch (retCode) + { + case ZLibReturnCode.Ok: + return "No error"; + case ZLibReturnCode.StreamEnd: + return "End of stream reaced"; + case ZLibReturnCode.NeedDictionary: + return "A preset dictionary is needed"; + case ZLibReturnCode.Errno: //consult error code + return "Unknown error " + Marshal.GetLastWin32Error(); + case ZLibReturnCode.StreamError: + return "Stream error"; + case ZLibReturnCode.DataError: + return "Data was corrupted"; + case ZLibReturnCode.MemoryError: + return "Out of memory"; + case ZLibReturnCode.BufferError: + return "Not enough room in provided buffer"; + case ZLibReturnCode.VersionError: + return "Incompatible zlib library version"; + default: + return "Unknown error"; + } + } + } + + + [Serializable] + public class ZLibException : ApplicationException + { + public ZLibException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + + public ZLibException(int errorCode) + : base(GetMsg(errorCode, null)) + { + + } + + public ZLibException(int errorCode, string lastStreamError) + : base(GetMsg(errorCode, lastStreamError)) + { + } + + private static string GetMsg(int errorCode, string lastStreamError) + { + string msg = "ZLib error " + errorCode + ": " + ZLibReturnCode.GetMesage(errorCode); + if (lastStreamError != null && lastStreamError.Length > 0) + msg += " (" + lastStreamError + ")"; + return msg; + } + } +} diff --git a/Flagrum.Core/Archive/ZLibNet/ZLibCompressors.cs b/Flagrum.Core/Archive/ZLibNet/ZLibCompressors.cs new file mode 100644 index 00000000..a1b84fe7 --- /dev/null +++ b/Flagrum.Core/Archive/ZLibNet/ZLibCompressors.cs @@ -0,0 +1,170 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.IO; + +namespace ZLibNet +{ + /// + /// Classes that simplify a common use of compression streams + /// + + delegate DeflateStream CreateStreamDelegate(Stream s, CompressionMode cm, bool leaveOpen); + + public static class DeflateCompressor + { + public static MemoryStream Compress(Stream source) + { + return CommonCompressor.Compress(CreateStream, source); + } + public static MemoryStream DeCompress(Stream source) + { + return CommonCompressor.DeCompress(CreateStream, source); + } + public static byte[] Compress(byte[] source) + { + return CommonCompressor.Compress(CreateStream, source); + } + public static byte[] DeCompress(byte[] source) + { + return CommonCompressor.DeCompress(CreateStream, source); + } + private static DeflateStream CreateStream(Stream s, CompressionMode cm, bool leaveOpen) + { + return new DeflateStream(s, cm, leaveOpen); + } + } + + public static class GZipCompressor + { + public static MemoryStream Compress(Stream source) + { + return CommonCompressor.Compress(CreateStream, source); + } + public static MemoryStream DeCompress(Stream source) + { + return CommonCompressor.DeCompress(CreateStream, source); + } + public static byte[] Compress(byte[] source) + { + return CommonCompressor.Compress(CreateStream, source); + } + public static byte[] DeCompress(byte[] source) + { + return CommonCompressor.DeCompress(CreateStream, source); + } + private static DeflateStream CreateStream(Stream s, CompressionMode cm, bool leaveOpen) + { + return new GZipStream(s, cm, leaveOpen); + } + } + + public static class ZLibCompressor + { + public static MemoryStream Compress(Stream source) + { + return CommonCompressor.Compress(CreateStream, source); + } + public static MemoryStream DeCompress(Stream source) + { + return CommonCompressor.DeCompress(CreateStream, source); + } + public static byte[] Compress(byte[] source) + { + return CommonCompressor.Compress(CreateStream, source); + } + public static byte[] DeCompress(byte[] source) + { + return CommonCompressor.DeCompress(CreateStream, source); + } + private static DeflateStream CreateStream(Stream s, CompressionMode cm, bool leaveOpen) + { + return new ZLibStream(s, cm, leaveOpen); + } + } + + public static class DynazipCompressor + { + const int DZ_DEFLATE_POS = 46; + + public static bool IsDynazip(byte[] source) + { + return source.Length >= 4 && BitConverter.ToInt32(source, 0) == 0x02014b50; + } + + public static byte[] DeCompress(byte[] source) + { + if (!IsDynazip(source)) + throw new InvalidDataException("not dynazip header"); + using (MemoryStream srcStream = new MemoryStream(source, DZ_DEFLATE_POS, source.Length - DZ_DEFLATE_POS)) + using (MemoryStream dstStream = DeCompress(srcStream)) + return dstStream.ToArray(); + } + + private static MemoryStream DeCompress(Stream source) + { + MemoryStream dest = new MemoryStream(); + DeCompress(source, dest); + dest.Position = 0; + return dest; + } + + private static void DeCompress(Stream source, Stream dest) + { + using (DeflateStream zsSource = new DeflateStream(source, CompressionMode.Decompress, true)) + { + zsSource.CopyTo(dest); + } + } + } + + class CommonCompressor + { + private static void Compress(CreateStreamDelegate sc, Stream source, Stream dest) + { + using (DeflateStream zsDest = sc(dest, CompressionMode.Compress, true)) + { + source.CopyTo(zsDest); + } + } + + private static void DeCompress(CreateStreamDelegate sc, Stream source, Stream dest) + { + using (DeflateStream zsSource = sc(source, CompressionMode.Decompress, true)) + { + zsSource.CopyTo(dest); + } + } + + public static MemoryStream Compress(CreateStreamDelegate sc, Stream source) + { + MemoryStream result = new MemoryStream(); + Compress(sc, source, result); + result.Position = 0; + return result; + } + + public static MemoryStream DeCompress(CreateStreamDelegate sc, Stream source) + { + MemoryStream result = new MemoryStream(); + DeCompress(sc, source, result); + result.Position = 0; + return result; + } + + public static byte[] Compress(CreateStreamDelegate sc, byte[] source) + { + using (MemoryStream srcStream = new MemoryStream(source)) + using (MemoryStream dstStream = Compress(sc, srcStream)) + return dstStream.ToArray(); + } + + public static byte[] DeCompress(CreateStreamDelegate sc, byte[] source) + { + using (MemoryStream srcStream = new MemoryStream(source)) + using (MemoryStream dstStream = DeCompress(sc, srcStream)) + return dstStream.ToArray(); + } + } +} diff --git a/Flagrum.Core/Archive/ZLibNet/ZLibDll.cs b/Flagrum.Core/Archive/ZLibNet/ZLibDll.cs new file mode 100644 index 00000000..74702693 --- /dev/null +++ b/Flagrum.Core/Archive/ZLibNet/ZLibDll.cs @@ -0,0 +1,21 @@ +using System; + +public static class ZLibDll +{ + internal const string Name32 = "zlib32.dll"; + internal const string Name64 = "zlib64.dll"; + + internal const string ZLibDllFileVersion = "1.2.8.1"; + + internal static bool Is64 = IntPtr.Size == 8; + + internal static string GetDllName() + { + if (Is64) + { + return Name64; + } + + return Name32; + } +} \ No newline at end of file diff --git a/Flagrum.Core/Archive/ZLibNet/ZLibStreams.cs b/Flagrum.Core/Archive/ZLibNet/ZLibStreams.cs new file mode 100644 index 00000000..6efdc263 --- /dev/null +++ b/Flagrum.Core/Archive/ZLibNet/ZLibStreams.cs @@ -0,0 +1,445 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; +using System.Runtime.Serialization; + +namespace ZLibNet +{ + /// Provides methods and properties used to compress and decompress streams. + public class DeflateStream : Stream + { + // private const int BufferSize = 16384; + + long pBytesIn = 0; + long pBytesOut = 0; + bool pSuccess; + // uint pCrcValue = 0; + const int WORK_DATA_SIZE = 0x1000; + byte[] pWorkData = new byte[WORK_DATA_SIZE]; + int pWorkDataPos = 0; + + private Stream pStream; + private CompressionMode pCompMode; + private z_stream pZstream = new z_stream(); + bool pLeaveOpen; + + public DeflateStream(Stream stream, CompressionMode mode) + : this(stream, mode, CompressionLevel.Default) + { + } + + public DeflateStream(Stream stream, CompressionMode mode, bool leaveOpen) : + this(stream, mode, CompressionLevel.Default, leaveOpen) + { + } + + public DeflateStream(Stream stream, CompressionMode mode, CompressionLevel level) : + this(stream, mode, level, false) + { + } + + public DeflateStream(Stream stream, CompressionMode compMode, CompressionLevel level, bool leaveOpen) + { + this.pLeaveOpen = leaveOpen; + this.pStream = stream; + this.pCompMode = compMode; + + int ret; + if (this.pCompMode == CompressionMode.Compress) + ret = ZLib.deflateInit(ref pZstream, level, this.WriteType); + else + ret = ZLib.inflateInit(ref pZstream, this.OpenType); + + if (ret != ZLibReturnCode.Ok) + throw new ZLibException(ret, pZstream.lasterrormsg); + + pSuccess = true; + } + + /// GZipStream destructor. Cleans all allocated resources. + ~DeflateStream() + { + this.Dispose(false); + } + + /// + /// Stream.Close() -> this.Dispose(true); + GC.SuppressFinalize(this); + /// Stream.Dispose() -> this.Close(); + /// + /// + protected override void Dispose(bool disposing) + { + try + { + try + { + if (disposing) //managed stuff + { + if (this.pStream != null) + { + //managed stuff + if (this.pCompMode == CompressionMode.Compress && pSuccess) + { + Flush(); + // this.pStream.Flush(); + } + if (!pLeaveOpen) + this.pStream.Close(); + this.pStream = null; + } + } + } + finally + { + //unmanaged stuff + FreeUnmanagedResources(); + } + + } + finally + { + base.Dispose(disposing); + } + } + + // Finished, free the resources used. + private void FreeUnmanagedResources() + { + if (this.pCompMode == CompressionMode.Compress) + ZLib.deflateEnd(ref pZstream); + else + ZLib.inflateEnd(ref pZstream); + } + + protected virtual ZLibOpenType OpenType + { + get { return ZLibOpenType.Deflate; } + } + protected virtual ZLibWriteType WriteType + { + get { return ZLibWriteType.Deflate; } + } + + + + /// Reads a number of decompressed bytes into the specified byte array. + /// The array used to store decompressed bytes. + /// The location in the array to begin reading. + /// The number of bytes decompressed. + /// The number of bytes that were decompressed into the byte array. If the end of the stream has been reached, zero or the number of bytes read is returned. + public override int Read(byte[] buffer, int offset, int count) + { + if (pCompMode == CompressionMode.Compress) + throw new NotSupportedException("Can't read on a compress stream!"); + + int readLen = 0; + if (pWorkDataPos != -1) + { + using (FixedArray workDataPtr = new FixedArray(pWorkData)) + using (FixedArray bufferPtr = new FixedArray(buffer)) + { + pZstream.next_in = workDataPtr[pWorkDataPos]; + pZstream.next_out = bufferPtr[offset]; + pZstream.avail_out = (uint)count; + + while (pZstream.avail_out != 0) + { + if (pZstream.avail_in == 0) + { + pWorkDataPos = 0; + pZstream.next_in = workDataPtr; + pZstream.avail_in = (uint)pStream.Read(pWorkData, 0, WORK_DATA_SIZE); + pBytesIn += pZstream.avail_in; + } + + uint inCount = pZstream.avail_in; + uint outCount = pZstream.avail_out; + + int zlibError = ZLib.inflate(ref pZstream, ZLibFlush.NoFlush); // flush method for inflate has no effect + + pWorkDataPos += (int)(inCount - pZstream.avail_in); + readLen += (int)(outCount - pZstream.avail_out); + + if (zlibError == ZLibReturnCode.StreamEnd) + { + pWorkDataPos = -1; // magic for StreamEnd + break; + } + else if (zlibError != ZLibReturnCode.Ok) + { + pSuccess = false; + throw new ZLibException(zlibError, pZstream.lasterrormsg); + } + } + + // pCrcValue = crc32(pCrcValue, &bufferPtr[offset], (uint)readLen); + pBytesOut += readLen; + } + + } + return readLen; + } + + + /// This property is not supported and always throws a NotSupportedException. + /// The array used to store compressed bytes. + /// The location in the array to begin reading. + /// The number of bytes compressed. + public override void Write(byte[] buffer, int offset, int count) + { + if (pCompMode == CompressionMode.Decompress) + throw new NotSupportedException("Can't write on a decompression stream!"); + + pBytesIn += count; + + using (FixedArray writePtr = new FixedArray(pWorkData)) + using (FixedArray bufferPtr = new FixedArray(buffer)) + { + pZstream.next_in = bufferPtr[offset]; + pZstream.avail_in = (uint)count; + pZstream.next_out = writePtr[pWorkDataPos]; + pZstream.avail_out = (uint)(WORK_DATA_SIZE - pWorkDataPos); + + // pCrcValue = crc32(pCrcValue, &bufferPtr[offset], (uint)count); + + while (pZstream.avail_in != 0) + { + if (pZstream.avail_out == 0) + { + //rar logikk, men det betyr vel bare at den kun skriver hvis buffer ble fyllt helt, + //dvs halvfyllt buffer vil kun skrives ved flush + pStream.Write(pWorkData, 0, (int)WORK_DATA_SIZE); + pBytesOut += WORK_DATA_SIZE; + pWorkDataPos = 0; + pZstream.next_out = writePtr; + pZstream.avail_out = WORK_DATA_SIZE; + } + + uint outCount = pZstream.avail_out; + + int zlibError = ZLib.deflate(ref pZstream, ZLibFlush.NoFlush); + + pWorkDataPos += (int)(outCount - pZstream.avail_out); + + if (zlibError != ZLibReturnCode.Ok) + { + pSuccess = false; + throw new ZLibException(zlibError, pZstream.lasterrormsg); + } + + } + } + } + + /// Flushes the contents of the internal buffer of the current GZipStream object to the underlying stream. + public override void Flush() + { + if (pCompMode == CompressionMode.Decompress) + throw new NotSupportedException("Can't flush a decompression stream."); + + using (FixedArray workDataPtr = new FixedArray(pWorkData)) + { + pZstream.next_in = IntPtr.Zero; + pZstream.avail_in = 0; + pZstream.next_out = workDataPtr[pWorkDataPos]; + pZstream.avail_out = (uint)(WORK_DATA_SIZE - pWorkDataPos); + + int zlibError = ZLibReturnCode.Ok; + while (zlibError != ZLibReturnCode.StreamEnd) + { + if (pZstream.avail_out != 0) + { + uint outCount = pZstream.avail_out; + zlibError = ZLib.deflate(ref pZstream, ZLibFlush.Finish); + + pWorkDataPos += (int)(outCount - pZstream.avail_out); + if (zlibError == ZLibReturnCode.StreamEnd) + { + //ok. will break loop + } + else if (zlibError != ZLibReturnCode.Ok) + { + pSuccess = false; + throw new ZLibException(zlibError, pZstream.lasterrormsg); + } + } + + pStream.Write(pWorkData, 0, pWorkDataPos); + pBytesOut += pWorkDataPos; + pWorkDataPos = 0; + pZstream.next_out = workDataPtr; + pZstream.avail_out = WORK_DATA_SIZE; + } + } + + this.pStream.Flush(); + } + + + //public uint CRC32 + //{ + // get + // { + // return pCrcValue; + // } + //} + + public long TotalIn + { + get { return this.pBytesIn; } + } + + public long TotalOut + { + get { return this.pBytesOut; } + } + + // The compression ratio obtained (same for compression/decompression). + public double CompressionRatio + { + get + { + if (pCompMode == CompressionMode.Compress) + return ((pBytesIn == 0) ? 0.0 : (100.0 - ((double)pBytesOut * 100.0 / (double)pBytesIn))); + else + return ((pBytesOut == 0) ? 0.0 : (100.0 - ((double)pBytesIn * 100.0 / (double)pBytesOut))); + } + } + + /// Gets a value indicating whether the stream supports reading while decompressing a file. + public override bool CanRead + { + get + { + return pCompMode == CompressionMode.Decompress && pStream.CanRead; + } + } + + /// Gets a value indicating whether the stream supports writing. + public override bool CanWrite + { + get + { + return pCompMode == CompressionMode.Compress && pStream.CanWrite; + } + } + + /// Gets a value indicating whether the stream supports seeking. + public override bool CanSeek + { + get { return (false); } + } + + /// Gets a reference to the underlying stream. + public Stream BaseStream + { + get { return (this.pStream); } + } + + /// This property is not supported and always throws a NotSupportedException. + /// The location in the stream. + /// One of the SeekOrigin values. + /// A long value. + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException("Seek not supported"); + } + + /// This property is not supported and always throws a NotSupportedException. + /// The length of the stream. + public override void SetLength(long value) + { + throw new NotSupportedException("SetLength not supported"); + } + + /// This property is not supported and always throws a NotSupportedException. + public override long Length + { + get + { + throw new NotSupportedException("Length not supported."); + } + } + + /// This property is not supported and always throws a NotSupportedException. + public override long Position + { + get + { + throw new NotSupportedException("Position not supported."); + } + set + { + throw new NotSupportedException("Position not supported."); + } + } + } + + /// + /// hdr(?) + adler32 et end. + /// wraps a deflate stream + /// + public class ZLibStream : DeflateStream + { + public ZLibStream(Stream stream, CompressionMode mode) + : base(stream, mode) + { + } + public ZLibStream(Stream stream, CompressionMode mode, bool leaveOpen) : + base(stream, mode, leaveOpen) + { + } + public ZLibStream(Stream stream, CompressionMode mode, CompressionLevel level) : + base(stream, mode, level) + { + } + public ZLibStream(Stream stream, CompressionMode mode, CompressionLevel level, bool leaveOpen) : + base(stream, mode, level, leaveOpen) + { + } + + protected override ZLibOpenType OpenType + { + get { return ZLibOpenType.ZLib; } + } + protected override ZLibWriteType WriteType + { + get { return ZLibWriteType.ZLib; } + } + } + + /// + /// Saved to file (.gz) can be opened with zip utils. + /// Have hdr + crc32 at end. + /// Wraps a deflate stream + /// + public class GZipStream : DeflateStream + { + public GZipStream(Stream stream, CompressionMode mode) + : base(stream, mode) + { + } + public GZipStream(Stream stream, CompressionMode mode, bool leaveOpen) : + base(stream, mode, leaveOpen) + { + } + public GZipStream(Stream stream, CompressionMode mode, CompressionLevel level) : + base(stream, mode, level) + { + } + public GZipStream(Stream stream, CompressionMode mode, CompressionLevel level, bool leaveOpen) : + base(stream, mode, level, leaveOpen) + { + } + + protected override ZLibOpenType OpenType + { + get { return ZLibOpenType.GZip; } + } + protected override ZLibWriteType WriteType + { + get { return ZLibWriteType.GZip; } + } + } + +} diff --git a/Flagrum.Core/Entities/NamedTreeNode.cs b/Flagrum.Core/Entities/NamedTreeNode.cs new file mode 100644 index 00000000..82ea52eb --- /dev/null +++ b/Flagrum.Core/Entities/NamedTreeNode.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace Flagrum.Core.Entities; + +[DataContract(IsReference = true)] +public class NamedTreeNode +{ + public NamedTreeNode(string name, NamedTreeNode parent) + { + Name = name; + Parent = parent; + Children = new LinkedList(); + } + + [DataMember] public string Name { get; set; } + + [DataMember] public NamedTreeNode Parent { get; set; } + + [DataMember] public LinkedList Children { get; set; } + + public NamedTreeNode AddChild(string name) + { + var node = new NamedTreeNode(name, this); + Children.AddFirst(new LinkedListNode(node)); + return node; + } + + public void TraverseAscending(Action visitor) + { + visitor(this); + foreach (var child in Children) + { + child.TraverseAscending(visitor); + } + } + + public void TraverseDescending(Action visitor) + { + visitor(this); + Parent?.TraverseDescending(visitor); + } +} \ No newline at end of file diff --git a/Flagrum.Core/Flagrum.Core.csproj b/Flagrum.Core/Flagrum.Core.csproj index 729d1d69..82275a9f 100644 --- a/Flagrum.Core/Flagrum.Core.csproj +++ b/Flagrum.Core/Flagrum.Core.csproj @@ -11,4 +11,9 @@ + + + + + diff --git a/Flagrum.Core/Gfxbin/Btex/BtexConverter.cs b/Flagrum.Core/Gfxbin/Btex/BtexConverter.cs index 918b8cb7..64f8ffc4 100644 --- a/Flagrum.Core/Gfxbin/Btex/BtexConverter.cs +++ b/Flagrum.Core/Gfxbin/Btex/BtexConverter.cs @@ -7,9 +7,12 @@ namespace Flagrum.Core.Gfxbin.Btex; public enum TextureType { - Color, - Greyscale, + Undefined, + BaseColor, + AmbientOcclusion, Normal, + Mrs, + Opacity, Preview, Thumbnail } @@ -249,7 +252,7 @@ private static byte ImageFlagsForType(TextureType type) { return type switch { - TextureType.Color => 49, + TextureType.BaseColor => 49, TextureType.Preview or TextureType.Thumbnail => 33, _ => 17 }; @@ -268,8 +271,10 @@ private static int GetBlockSize(TextureType type) { return type switch { - TextureType.Color => 2, - TextureType.Greyscale => 2, + TextureType.BaseColor => 2, + TextureType.AmbientOcclusion => 2, + TextureType.Mrs => 2, + TextureType.Undefined => 2, _ => 4 }; } diff --git a/Flagrum.Core/Gfxbin/Btex/BtexHelper.cs b/Flagrum.Core/Gfxbin/Btex/BtexHelper.cs index 75bf2880..edba62fa 100644 --- a/Flagrum.Core/Gfxbin/Btex/BtexHelper.cs +++ b/Flagrum.Core/Gfxbin/Btex/BtexHelper.cs @@ -12,13 +12,22 @@ public static TextureType GetType(string textureId) } if (textureId.Contains("basecolor", StringComparison.OrdinalIgnoreCase) - || textureId.Contains("mrs", StringComparison.OrdinalIgnoreCase) || textureId.Contains("emissive", StringComparison.OrdinalIgnoreCase)) { - return TextureType.Color; + return TextureType.BaseColor; } - return TextureType.Greyscale; + if (textureId.Contains("mrs", StringComparison.OrdinalIgnoreCase)) + { + return TextureType.Mrs; + } + + if (textureId.Contains("occlusion", StringComparison.OrdinalIgnoreCase)) + { + return TextureType.AmbientOcclusion; + } + + return TextureType.Undefined; } public static string GetSuffix(string textureId) diff --git a/Flagrum.Core/Gfxbin/Gmdl/ModelReplacer.cs b/Flagrum.Core/Gfxbin/Gmdl/ModelReplacer.cs index 4bd39913..a69391ea 100644 --- a/Flagrum.Core/Gfxbin/Gmdl/ModelReplacer.cs +++ b/Flagrum.Core/Gfxbin/Gmdl/ModelReplacer.cs @@ -1,5 +1,5 @@ using System; -using System.IO; +using System.Collections.Generic; using System.Linq; using Flagrum.Core.Gfxbin.Gmdl.Components; using Flagrum.Core.Gfxbin.Gmdl.Constructs; @@ -21,8 +21,10 @@ public ModelReplacer(Model originalModel, Gpubin replacementData) _gpubin = replacementData; } - public Model Replace() + public Model Replace(bool isModelReplacement) { + var usedIndices = new List(); + foreach (var mesh in _model.MeshObjects[0].Meshes) { var match = _gpubin.Meshes.FirstOrDefault(m => m.Name == mesh.Name); @@ -127,33 +129,6 @@ public Model Replace() } } - for (var i = 0; i < mesh.VertexCount; i++) - { - if (mesh.WeightLimit == 6) - { - var weights = 0; - for (var j = 0; j < 4; j++) - { - if (j < weightValues[0][i].Length && weightValues[0][i][j] > 0) - { - weights++; - } - - if (j < weightValues[1][i].Length && weightValues[1][i][j] > 0) - { - weights++; - } - } - - // var sum1 = weightValues[0][i].Sum(w => w); - // var sum2 = weightValues[1][i].Sum(w => w); - if (weights > 6) - { - File.AppendAllText(@"C:\Modding\MaterialTesting\log.txt", "REEEEEEEEE\r\n"); - } - } - } - // Replace model data with the imported data mesh.Normals = match.Normals; mesh.Tangents = match.Tangents; @@ -199,21 +174,72 @@ public Model Replace() new Vector3(0, 0, mesh.Aabb.Max.Z - center.Z) ); - mesh.BoneIds = _gpubin.BoneTable.Count > 1 - ? Enumerable.Range(0, _gpubin.BoneTable.Max(m => m.Key) - 1).Select(i => (uint)i) - : new[] {0u}; + if (isModelReplacement) + { + mesh.BoneIds = mesh.WeightIndices + .SelectMany(w => w + .SelectMany(x => x + .Select(y => (uint)y))) + .Distinct() + .OrderBy(b => b); + + usedIndices.AddRange(mesh.BoneIds); + } + else + { + mesh.BoneIds = _gpubin.BoneTable.Count > 1 + ? Enumerable.Range(0, _gpubin.BoneTable.Max(m => m.Key) - 1).Select(i => (uint)i) + : new[] {0u}; + } } } - _model.BoneHeaders = _gpubin.BoneTable - .Select(kvp => + if (isModelReplacement) + { + // Create arbitrary indices for the bones + // Start at 10000 to avoid conflicts with other loaded bones on the target model + ushort count = 10000; + usedIndices = usedIndices.Distinct().ToList(); + var indexMap = usedIndices.ToDictionary(i => i, i => count++); + + // Update each weight index in the mesh to match the new index map + foreach (var mesh in _model.MeshObjects[0].Meshes) { - var (boneIndex, boneName) = kvp; - boneIndex = ushort.MaxValue; - var lodIndex = ((uint)boneIndex << 16) | 0xFFFF; - return new BoneHeader {LodIndex = lodIndex, Name = boneName}; - }) - .ToList(); + foreach (var indexList in mesh.WeightIndices) + { + foreach (var indices in indexList) + { + for (var i = 0; i < indices.Length; i++) + { + indices[i] = indexMap[indices[i]]; + } + } + } + } + + // Generate the fixed bone table and apply it to the model + _model.BoneHeaders = _gpubin.BoneTable + .Where(d => usedIndices.Contains((ushort)d.Key)) + .Select(kvp => new BoneHeader + { + Name = kvp.Value, + LodIndex = ((uint)indexMap[(ushort)kvp.Key] << 16) | 0xFFFF + }) + .OrderBy(b => b.LodIndex) + .ToList(); + } + else + { + _model.BoneHeaders = _gpubin.BoneTable + .Select(kvp => + { + var (boneIndex, boneName) = kvp; + boneIndex = ushort.MaxValue; + var lodIndex = ((uint)boneIndex << 16) | 0xFFFF; + return new BoneHeader {LodIndex = lodIndex, Name = boneName}; + }) + .ToList(); + } return _model; } diff --git a/Flagrum.Core/Utilities/IOHelper.cs b/Flagrum.Core/Utilities/IOHelper.cs index 04ae6bcf..fa4c9393 100644 --- a/Flagrum.Core/Utilities/IOHelper.cs +++ b/Flagrum.Core/Utilities/IOHelper.cs @@ -12,10 +12,6 @@ public static string GetExecutingDirectory() public static string GetWebRoot() { -#if DEBUG return $"{GetExecutingDirectory()}\\wwwroot"; -#else - return $"{GetExecutingDirectory()}\\wwwroot\\_content\\Flagrum.Web"; -#endif } } \ No newline at end of file diff --git a/Flagrum.Core/Utilities/Serialization.cs b/Flagrum.Core/Utilities/Serialization.cs index b3b85ad6..2f140345 100644 --- a/Flagrum.Core/Utilities/Serialization.cs +++ b/Flagrum.Core/Utilities/Serialization.cs @@ -1,4 +1,5 @@ using System; +using System.Text; namespace Flagrum.Core.Utilities; @@ -52,4 +53,10 @@ public static string ToSafeString(this string input) return string.IsNullOrWhiteSpace(input) ? Guid.NewGuid().ToString().ToLower() : output.ToLower(); } + + public static string ToBase64(this string input) + { + var bytes = Encoding.UTF8.GetBytes(input); + return Convert.ToBase64String(bytes); + } } \ No newline at end of file diff --git a/Flagrum.Core/zlib64.dll b/Flagrum.Core/zlib64.dll new file mode 100644 index 0000000000000000000000000000000000000000..20233f2f2518a266b7e86ae9602a681a1066c7ce GIT binary patch literal 165376 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�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 zResources\flagrum.ico embedded Flagrum - 1.1.4 - 1.1.4 + 1.1.5 + 1.1.5 @@ -45,6 +45,33 @@ PreserveNewest + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + diff --git a/Flagrum.Desktop/MainWindow.xaml.cs b/Flagrum.Desktop/MainWindow.xaml.cs index 5158989f..52b0f3b6 100644 --- a/Flagrum.Desktop/MainWindow.xaml.cs +++ b/Flagrum.Desktop/MainWindow.xaml.cs @@ -27,12 +27,8 @@ public partial class MainWindow : Window, INotifyPropertyChanged { private readonly string flagrumDirectory; private readonly string logFile; - -#if DEBUG + public string HostPageUri { get; } = "wwwroot/index.html"; -#else - public string HostPageUri { get; } = "wwwroot/_content/Flagrum.Web/index.html"; -#endif private bool _hasWebView2Runtime; diff --git a/Flagrum.Web/wwwroot/app.css b/Flagrum.Desktop/wwwroot/app.css similarity index 100% rename from Flagrum.Web/wwwroot/app.css rename to Flagrum.Desktop/wwwroot/app.css diff --git a/Flagrum.Web/wwwroot/bmc.js b/Flagrum.Desktop/wwwroot/bmc.js similarity index 100% rename from Flagrum.Web/wwwroot/bmc.js rename to Flagrum.Desktop/wwwroot/bmc.js diff --git a/Flagrum.Web/wwwroot/discord-button.png b/Flagrum.Desktop/wwwroot/discord-button.png similarity index 100% rename from Flagrum.Web/wwwroot/discord-button.png rename to Flagrum.Desktop/wwwroot/discord-button.png diff --git a/Flagrum.Web/wwwroot/fonts/MaterialIcons-Regular.ttf b/Flagrum.Desktop/wwwroot/fonts/MaterialIcons-Regular.ttf similarity index 100% rename from Flagrum.Web/wwwroot/fonts/MaterialIcons-Regular.ttf rename to Flagrum.Desktop/wwwroot/fonts/MaterialIcons-Regular.ttf diff --git a/Flagrum.Web/wwwroot/fonts/MaterialIconsOutlined-Regular.otf b/Flagrum.Desktop/wwwroot/fonts/MaterialIconsOutlined-Regular.otf similarity index 100% rename from Flagrum.Web/wwwroot/fonts/MaterialIconsOutlined-Regular.otf rename to Flagrum.Desktop/wwwroot/fonts/MaterialIconsOutlined-Regular.otf diff --git a/Flagrum.Web/wwwroot/fonts/Play-Regular.ttf b/Flagrum.Desktop/wwwroot/fonts/Play-Regular.ttf similarity index 100% rename from Flagrum.Web/wwwroot/fonts/Play-Regular.ttf rename to Flagrum.Desktop/wwwroot/fonts/Play-Regular.ttf diff --git a/Flagrum.Web/wwwroot/fonts/Roboto-Regular.ttf b/Flagrum.Desktop/wwwroot/fonts/Roboto-Regular.ttf similarity index 100% rename from Flagrum.Web/wwwroot/fonts/Roboto-Regular.ttf rename to Flagrum.Desktop/wwwroot/fonts/Roboto-Regular.ttf diff --git a/Flagrum.Web/wwwroot/index.html b/Flagrum.Desktop/wwwroot/index.html similarity index 64% rename from Flagrum.Web/wwwroot/index.html rename to Flagrum.Desktop/wwwroot/index.html index 1be5fbd4..fd775cd9 100644 --- a/Flagrum.Web/wwwroot/index.html +++ b/Flagrum.Desktop/wwwroot/index.html @@ -8,6 +8,28 @@ + + + @@ -21,6 +43,7 @@ + diff --git a/Flagrum.Web/wwwroot/interop.js b/Flagrum.Desktop/wwwroot/interop.js similarity index 100% rename from Flagrum.Web/wwwroot/interop.js rename to Flagrum.Desktop/wwwroot/interop.js diff --git a/Flagrum.Web/App.razor b/Flagrum.Web/App.razor index 6f2668d1..0195f06f 100644 --- a/Flagrum.Web/App.razor +++ b/Flagrum.Web/App.razor @@ -1,4 +1,15 @@ +@using Microsoft.EntityFrameworkCore +@using System.Collections.Concurrent +@using System.IO +@using Flagrum.Core.Archive +@using Flagrum.Core.Utilities @inject JSInterop Interop +@inject FlagrumDbContext Context +@inject UriMapper UriMapper +@inject AppStateService AppState +@inject SettingsService Settings +@inject ILogger Logger +@inject BinmodTypeHelper BinmodTypeHelper @@ -9,4 +20,115 @@

Sorry, there's nothing at this address.

-
\ No newline at end of file + + +@code +{ + protected override async Task OnInitializedAsync() + { + await Context.Database.MigrateAsync(); + await LoadBinmods(); + StateHasChanged(); + await LoadNodes(); + } + + private async Task LoadNodes() + { + if (!Context.AssetExplorerNodes.Any()) + { + await Task.Run(() => + { + UriMapper.RegenerateMap(); + AppState.Node = Context.AssetExplorerNodes + .FirstOrDefault(n => n.Id == 1); + }); + } + else + { + AppState.Node = Context.AssetExplorerNodes + .FirstOrDefault(n => n.Id == 1); + } + } + + private async Task LoadBinmods() + { + if (!AppState.IsModListInitialized) + { + await Task.Run(() => + { + var binmodList = ModlistEntry.FromFile(Settings.BinmodListPath); + var localMods = Directory.GetFiles(Settings.ModDirectory, "*.ffxvbinmod", SearchOption.TopDirectoryOnly); + IEnumerable allMods; + + if (Directory.Exists(Settings.WorkshopDirectory)) + { + var workshopMods = Directory.GetFiles(Settings.WorkshopDirectory, "*.ffxvbinmod", SearchOption.AllDirectories); + allMods = localMods.Union(workshopMods); + } + else + { + allMods = localMods; + } + + var mods = new ConcurrentBag(); + + Parallel.ForEach(allMods, file => + { + using var unpacker = new Unpacker(file); + var modmetaBytes = unpacker.UnpackFileByQuery("index.modmeta", out _); + var mod = Binmod.FromModmetaBytes(modmetaBytes, BinmodTypeHelper, Logger); + var previewBytes = unpacker.UnpackFileByQuery("$preview.png.bin", out _); + + var binmodListing = binmodList.FirstOrDefault(e => file.Contains(e.Path.Replace('/', '\\'))); + + if (mod == null) + { + Logger.LogWarning("Could not read modmeta from {File}", file); + return; + } + + if (binmodListing == null) + { + Logger.LogWarning("Could not find binmod.list entry for {File}", file); + return; + } + + mod.Description = mod.Description?.Replace("\\n", "\n"); + mod.GameMenuDescription = mod.GameMenuDescription?.Replace("\\n", "\n"); + mod.IsWorkshopMod = binmodListing.IsWorkshopMod; + mod.Index = binmodListing.Index; + mod.IsApplyToGame = binmodListing.IsEnabled; + mod.Path = file; + File.WriteAllBytes($"{IOHelper.GetWebRoot()}\\images\\{mod.Uuid}.png", previewBytes); + + mods.Add(mod); + }); + + AppState.Mods = mods.ToList(); + var paths = AppState.Mods.Select(m => m.Path); + AppState.UnmanagedEntries = binmodList.Where(e => !paths.Any(p => p.Contains(e.Path.Replace('/', '\\')))).ToList(); + }); + + ClearOldImages(); + AppState.IsModListInitialized = true; + } + } + + private async void ClearOldImages() + { + await Task.Run(() => + { + var exceptions = AppState.Mods + .Select(m => $"{IOHelper.GetWebRoot()}\\images\\{m.Uuid}.png") + .ToList(); + + foreach (var image in Directory.EnumerateFiles($"{IOHelper.GetWebRoot()}\\images")) + { + if (!exceptions.Contains(image)) + { + File.Delete(image); + } + } + }); + } +} \ No newline at end of file diff --git a/Flagrum.Web/Components/Controls/LoadingIndicator.razor b/Flagrum.Web/Components/Controls/LoadingIndicator.razor new file mode 100644 index 00000000..92ccd249 --- /dev/null +++ b/Flagrum.Web/Components/Controls/LoadingIndicator.razor @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/Flagrum.Web/Features/ModLibrary/Components/ModTypeButton.razor b/Flagrum.Web/Components/Controls/ModTypeButton.razor similarity index 100% rename from Flagrum.Web/Features/ModLibrary/Components/ModTypeButton.razor rename to Flagrum.Web/Components/Controls/ModTypeButton.razor diff --git a/Flagrum.Web/Features/ModLibrary/Components/ModTypeButtonGroup.razor b/Flagrum.Web/Components/Controls/ModTypeButtonGroup.razor similarity index 100% rename from Flagrum.Web/Features/ModLibrary/Components/ModTypeButtonGroup.razor rename to Flagrum.Web/Components/Controls/ModTypeButtonGroup.razor diff --git a/Flagrum.Web/Features/AssetExplorer/Data/AssetExplorerItem.cs b/Flagrum.Web/Features/AssetExplorer/Data/AssetExplorerItem.cs new file mode 100644 index 00000000..e2116505 --- /dev/null +++ b/Flagrum.Web/Features/AssetExplorer/Data/AssetExplorerItem.cs @@ -0,0 +1,73 @@ +using System; +using System.IO; +using System.Linq; +using Flagrum.Core.Archive; +using Flagrum.Web.Persistence; +using Flagrum.Web.Persistence.Entities; +using Flagrum.Web.Services; + +namespace Flagrum.Web.Features.AssetExplorer.Data; + +public class AssetExplorerItem +{ + public string Name { get; set; } + public ExplorerItemType Type { get; set; } + public Func Data { get; set; } + + public string DisplayName + { + get + { + if (Type == ExplorerItemType.Directory || Name.StartsWith("CRAF")) + { + return Name; + } + + var extension = Name[Name.LastIndexOf('.')..].ToLower(); + var trueExtension = extension switch + { + ".tif" or ".tga" or ".png" or ".dds" or ".exr" => ".btex", + ".gmtl" => ".gmtl.gfxbin", + ".gmdl" => ".gmdl.gfxbin", + _ => extension + }; + + return Name[..Name.LastIndexOf('.')] + trueExtension; + } + } + + public static AssetExplorerItem FromNode(AssetExplorerNode node, FlagrumDbContext context) + { + var uri = node.GetUri(context); + return new AssetExplorerItem + { + Name = node.Name, + Data = () => context.GetFileByUri(uri), + Type = context.AssetExplorerNodes.Any(n => n.ParentId == node.Id) + ? ExplorerItemType.Directory + : GetType(uri) + }; + } + + public static AssetExplorerItem FromExplorerItem(ExplorerItem item) + { + return new AssetExplorerItem + { + Name = item.Name, + Type = item.Type, + Data = () => File.ReadAllBytes(item.Path) + }; + } + + public static ExplorerItemType GetType(string uri) + { + var extension = uri.Split('/').Last().Split('.').Last(); + return extension switch + { + "btex" or "dds" or "png" or "tif" or "tga" or "exr" => ExplorerItemType.Texture, + "gmtl" => ExplorerItemType.Material, + "gmdl" => ExplorerItemType.Model, + _ => ExplorerItemType.Unsupported + }; + } +} \ No newline at end of file diff --git a/Flagrum.Web/Features/AssetExplorer/Data/ExplorerItem.cs b/Flagrum.Web/Features/AssetExplorer/Data/ExplorerItem.cs index 94df6cf3..b26889ac 100644 --- a/Flagrum.Web/Features/AssetExplorer/Data/ExplorerItem.cs +++ b/Flagrum.Web/Features/AssetExplorer/Data/ExplorerItem.cs @@ -2,6 +2,7 @@ public enum ExplorerItemType { + Unspecified, Unsupported, Directory, Material, diff --git a/Flagrum.Web/Features/AssetExplorer/ExplorerItemRow.razor b/Flagrum.Web/Features/AssetExplorer/ExplorerItemRow.razor index 971d9395..c5387d4b 100644 --- a/Flagrum.Web/Features/AssetExplorer/ExplorerItemRow.razor +++ b/Flagrum.Web/Features/AssetExplorer/ExplorerItemRow.razor @@ -1,6 +1,4 @@ @using Flagrum.Web.Features.AssetExplorer.Data -@using System.IO -@using Flagrum.Core.Utilities @implements IDisposable @inject JSInterop Interop @inject AppStateService AppState @@ -77,20 +75,9 @@ private void OnClick() { - switch (Item.Type) + if (Item.Type == ExplorerItemType.Directory) { - case ExplorerItemType.Directory: - Parent.SetActiveDirectory(Item.Path); - break; - case ExplorerItemType.Texture: - PreviewTexture(); - break; - case ExplorerItemType.Material: - Parent.SetPreviewMaterial(Item.Path); - break; - case ExplorerItemType.Model: - Parent.SetPreviewModel(Item.Path); - break; + Parent.SetActiveDirectory(Item.Path); } Parent.SetSelectedItem(this); @@ -102,14 +89,6 @@ StateHasChanged(); } - private void PreviewTexture() - { - var converter = new TextureConverter(); - var jpegBytes = converter.BtexToJpg(File.ReadAllBytes(Item.Path)); - File.WriteAllBytes($"{IOHelper.GetWebRoot()}\\images\\asset_preview.jpg", jpegBytes); - Parent.SetPreviewImage(); - } - private async void OnInitialLoad(object sender, EventArgs e) { var path = DirectParent.InitialPath; diff --git a/Flagrum.Web/Features/AssetExplorer/Export/BatchExportModal.razor b/Flagrum.Web/Features/AssetExplorer/Export/BatchExportModal.razor new file mode 100644 index 00000000..d1031d90 --- /dev/null +++ b/Flagrum.Web/Features/AssetExplorer/Export/BatchExportModal.razor @@ -0,0 +1,110 @@ +@using Index = Flagrum.Web.Features.AssetExplorer.Index +@using System.IO +@inject IWpfService WpfService + + + + Batch Export + cancel + + +
+ Batch export currently only supports textures. +
+ + +
+
+ + Also export from subdirectories recursively +
+
+
+
+ +@code +{ + [CascadingParameter] + public Index Parent { get; set; } + + private Modal Modal { get; set; } + private bool IsRecursive { get; set; } + private string Extension { get; set; } = "PNG"; + + public void Open() + { + Modal.Open(); + } + + private async Task ExportBatch() + { + await WpfService.OpenFolderDialogAsync(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), folder => + { + InvokeAsync(() => + { + Modal.Close(); + Parent.IsLoading = true; + StateHasChanged(); + + var files = new List(); + + if (IsRecursive) + { + BuildFileListRecursively(Parent.SelectedDirectory.Item.Path, files); + } + else + { + files.AddRange(Directory.GetFiles(Parent.SelectedDirectory.Item.Path, "*.btex")); + } + + var directories = files + .Select(f => + { + var relativePath = f.Replace(Parent.SelectedDirectory.Item.Path, "")[1..]; + var index = relativePath.LastIndexOf('\\'); + return index > 0 ? relativePath.Remove(index) : null; + }) + .Where(d => !string.IsNullOrWhiteSpace(d)) + .Distinct(); + + foreach (var directory in directories) + { + Directory.CreateDirectory($"{folder}\\{directory}"); + } + + Parallel.ForEach(files, file => + { + var relativePath = file.Replace(Parent.SelectedDirectory.Item.Path, ""); + var outputPath = $"{folder}{relativePath.Replace(".btex", $".{Extension.ToLower()}")}"; + var btex = File.ReadAllBytes(file); + var converter = new TextureConverter(); + var data = Extension switch + { + "PNG" => converter.BtexToPng(btex), + "TGA" => converter.BtexToTga(btex), + _ => converter.BtexToDds(btex) + }; + + File.WriteAllBytes(outputPath, data); + }); + + Parent.IsLoading = false; + StateHasChanged(); + }); + }); + } + + private void BuildFileListRecursively(string directory, List files) + { + files.AddRange(Directory.GetFiles(directory, "*.btex")); + foreach (var subdirectory in Directory.EnumerateDirectories(directory)) + { + BuildFileListRecursively(subdirectory, files); + } + } +} \ No newline at end of file diff --git a/Flagrum.Web/Features/AssetExplorer/Export/ExportContextMenu.razor b/Flagrum.Web/Features/AssetExplorer/Export/ExportContextMenu.razor new file mode 100644 index 00000000..2932d004 --- /dev/null +++ b/Flagrum.Web/Features/AssetExplorer/Export/ExportContextMenu.razor @@ -0,0 +1,100 @@ +@using Index = Flagrum.Web.Features.AssetExplorer.Index +@using Flagrum.Web.Features.AssetExplorer.Data +@using System.IO +@inject IWpfService WpfService +@inject FlagrumDbContext Context +@inject SettingsService Settings +@inject ILogger Logger + + + @if (Parent.ContextItem.Type == ExplorerItemType.Directory) + { + +
+ drive_file_move + Export Folder +
+
+ } + else + { + +
+ open_in_new + Export File +
+
+ + if (Parent.ContextItem.Type == ExplorerItemType.Model) + { + +
+ share + Export With Dependencies +
+
+ } + } +
+ + + + +@code +{ + [CascadingParameter] + public Index Parent { get; set; } + + private ExportWithDependenciesModal ExportWithDependenciesModal { get; set; } + private ExportFolderModal ExportFolderModal { get; set; } + + private async Task ExportSingle() + { + var item = Parent.ContextItem; + + var defaultName = item.Type switch + { + ExplorerItemType.Material => item.Name + ".gfxbin", + ExplorerItemType.Model => item.Name + ".gfxbin", + ExplorerItemType.Texture => item.Name[..item.Name.LastIndexOf('.')] + ".png", + _ => item.Name + }; + + var filter = item.Type switch + { + ExplorerItemType.Material => "Game Material|*.gmtl.gfxbin", + ExplorerItemType.Model => "Game Model|*.gmdl.gfxbin", + ExplorerItemType.Texture => "PNG Image|*.png|TGA Image|*.tga|DDS Image|*.dds|BTEX Image|*.btex", + _ => $"Unknown File|*.{item.Name.Split('.').Last()}" + }; + + await WpfService.OpenSaveFileDialogAsync(defaultName, filter, path => + { + byte[] data; + + if (item.Type == ExplorerItemType.Texture) + { + var extension = path.Split('.').Last(); + var converter = new TextureConverter(); + data = extension switch + { + "png" => converter.BtexToPng(item.Data()), + "tga" => converter.BtexToTga(item.Data()), + "dds" => converter.BtexToDds(item.Data()), + _ => item.Data() + }; + } + else + { + data = item.Data(); + } + + File.WriteAllBytes(path, data); + }); + } +} \ No newline at end of file diff --git a/Flagrum.Web/Features/AssetExplorer/Export/ExportFolderModal.razor b/Flagrum.Web/Features/AssetExplorer/Export/ExportFolderModal.razor new file mode 100644 index 00000000..1edc6673 --- /dev/null +++ b/Flagrum.Web/Features/AssetExplorer/Export/ExportFolderModal.razor @@ -0,0 +1,144 @@ +@using Index = Flagrum.Web.Features.AssetExplorer.Index +@using System.IO +@using Flagrum.Core.Archive +@using Flagrum.Web.Features.AssetExplorer.Data +@using Flagrum.Web.Persistence.Entities +@inject IWpfService WpfService +@inject FlagrumDbContext Context +@inject SettingsService Settings + + + + Export Folder + cancel + + +
+
+ + +
+
+ + Also export from subdirectories recursively +
+
+
+
+ +@code +{ + [CascadingParameter] + public Index Parent { get; set; } + + private Modal Modal { get; set; } + private bool IsRecursive { get; set; } + private string Extension { get; set; } = "PNG"; + + public void Open() + { + Modal.Open(); + } + + private async Task Export() + { + await WpfService.OpenFolderDialogAsync(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), folder => + { + InvokeAsync(() => + { + Modal.Close(); + Parent.SetLoading(true, "Exporting"); + + var files = new Dictionary>(); + + if (IsRecursive) + { + Parent.ContextNode.Traverse(Context, node => + { + if (!Context.AssetExplorerNodes.Any(n => n.ParentId == node.Id)) + { + var location = node.GetLocation(Context, Settings); + if (!files.TryGetValue(location, out var uris)) + { + uris = new List {node.GetUri(Context)}; + files.Add(location, uris); + } + else + { + uris.Add(node.GetUri(Context)); + } + } + }); + } + else + { + foreach (var node in Parent.ContextNode.Children) + { + if (!Context.AssetExplorerNodes.Any(n => n.ParentId == node.Id)) + { + var location = node.GetLocation(Context, Settings); + if (!files.TryGetValue(location, out var uris)) + { + uris = new List {node.GetUri(Context)}; + files.Add(location, uris); + } + else + { + uris.Add(node.GetUri(Context)); + } + } + } + } + + var baseUri = Parent.ContextNode.GetUri(Context); + var converter = new TextureConverter(); + + foreach (var (archiveLocation, uris) in files) + { + using var unpacker = new Unpacker(archiveLocation); + foreach (var uri in uris) + { + var data = unpacker.UnpackFileByQuery(uri, out _); + if (AssetExplorerItem.GetType(uri) == ExplorerItemType.Texture) + { + data = Extension switch + { + "PNG" => converter.BtexToPng(data), + "TGA" => converter.BtexToTga(data), + "DDS" => converter.BtexToDds(data), + _ => data + }; + } + + var relativePath = Parent.ContextNode.Name + "\\" + uri + .Replace(baseUri, "") + .Replace('/', '\\'); + + var currentDirectory = folder; + var split = relativePath.Split('\\'); + for (var index = 0; index < split.Length - 1; index++) + { + var token = split[index]; + currentDirectory += "\\" + token; + if (!Directory.Exists(currentDirectory)) + { + Directory.CreateDirectory(currentDirectory); + } + } + + var absolutePath = currentDirectory + "\\" + split.Last(); + File.WriteAllBytes(absolutePath, data); + } + } + + Parent.SetLoading(false); + }); + }); + } +} \ No newline at end of file diff --git a/Flagrum.Web/Features/AssetExplorer/Export/ExportWithDependenciesModal.razor b/Flagrum.Web/Features/AssetExplorer/Export/ExportWithDependenciesModal.razor new file mode 100644 index 00000000..28ba8bed --- /dev/null +++ b/Flagrum.Web/Features/AssetExplorer/Export/ExportWithDependenciesModal.razor @@ -0,0 +1,186 @@ +@using Flagrum.Web.Persistence.Entities +@using Index = Flagrum.Web.Features.AssetExplorer.Index +@using Flagrum.Core.Gfxbin.Data +@using Flagrum.Core.Gfxbin.Gmtl +@using Flagrum.Core.Archive +@using System.Text +@using System.IO +@using BinaryReader = Flagrum.Core.Gfxbin.Serialization.BinaryReader +@inject IWpfService WpfService +@inject SettingsService Settings +@inject FlagrumDbContext Context + + + + Export With Dependencies + cancel + + +
+
+ + +
+
+ + Include high resolution textures +
+
+ + Include shared textures +
+
+
+
+ +@code +{ + [CascadingParameter] + public Index Parent { get; set; } + + private Modal Modal { get; set; } + private string Extension { get; set; } = "PNG"; + private bool IncludeHighTextures { get; set; } = true; + private bool IncludeCommonTextures { get; set; } + + public void Open() + { + Modal.Open(); + } + + private async Task Export() + { + var item = Parent.ContextItem; + var defaultName = item.Name + ".gfxbin"; + const string filter = "Game Model|*.gmdl.gfxbin"; + + await WpfService.OpenSaveFileDialogAsync(defaultName, filter, path => + { + InvokeAsync(() => + { + Parent.SetLoading(true, "Exporting"); + Modal.Close(); + }); + + var gfxbin = item.Data(); + var reader = new BinaryReader(gfxbin); + var header = new GfxbinHeader(); + header.Read(reader); + + var materials = new Dictionary(); + var textures = new Dictionary(); + + var gpubinUri = header.Dependencies.FirstOrDefault(d => d.Path.EndsWith(".gpubin"))?.Path; + var gpubin = Context.GetFileByUri(gpubinUri); + + var materialUris = header.Dependencies + .Where(d => d.Path.EndsWith(".gmtl")) + .Select(d => d.Path); + + foreach (var uri in materialUris) + { + var materialData = Context.GetFileByUri(uri); + materials.Add(uri, materialData); + + var material = new MaterialReader(materialData).Read(); + foreach (var texture in material.Textures.Where(t => !string.IsNullOrEmpty(t.Path) && !t.Path.EndsWith(".sb"))) + { + if (textures.ContainsKey(texture.Path) || !CommonCheck(texture.Path)) + { + continue; + } + + var textureData = Context.GetFileByUri(texture.Path); + textures.Add(texture.Path, ConvertTexture(textureData)); + } + + if (IncludeHighTextures) + { + var highTexturePackUri = material.HighTexturePackAsset.Replace(".htpk", ".autoext"); + var highTexturePack = Context.GetFileByUri(highTexturePackUri); + + foreach (var highTexture in Encoding.UTF8.GetString(highTexturePack) + .Split(' ') + .Where(s => !string.IsNullOrWhiteSpace(s)) + .Select(s => + { + var result = s.Trim(); + if (result.Last() == 0x00) + { + result = result[..^1]; + } + + return result; + })) + { + if (textures.ContainsKey(highTexture) || !CommonCheck(highTexture)) + { + continue; + } + + var textureData = Context.GetFileByUri(highTexture); + textures.Add(highTexture, ConvertTexture(textureData)); + } + } + } + + File.WriteAllBytes(path, gfxbin); + File.WriteAllBytes(path.Replace(".gmdl.gfxbin", ".gpubin"), gpubin); + var root = Path.GetDirectoryName(path); + var materialsDirectory = $"{root}\\materials"; + var texturesDirectory = $"{root}\\textures"; + + if (!Directory.Exists(materialsDirectory)) + { + Directory.CreateDirectory(materialsDirectory); + } + + if (!Directory.Exists(texturesDirectory)) + { + Directory.CreateDirectory(texturesDirectory); + } + + foreach (var (uri, data) in materials) + { + var materialPath = $"{materialsDirectory}\\{uri.Split('/').Last()}.gfxbin"; + File.WriteAllBytes(materialPath, data); + } + + foreach (var (uri, data) in textures) + { + var fileName = uri.Split('/').Last(); + fileName = fileName[..fileName.LastIndexOf('.')] + "." + Extension.ToLower(); + var texturePath = $"{texturesDirectory}\\{fileName}"; + File.WriteAllBytes(texturePath, data); + } + + InvokeAsync(() => Parent.SetLoading(false)); + }); + } + + private bool CommonCheck(string uri) + { + return IncludeCommonTextures + || !uri.StartsWith("data://shader") + && !uri.StartsWith("data://vfx") + && !uri.Contains("/common/"); + } + + private byte[] ConvertTexture(byte[] btex) + { + var converter = new TextureConverter(); + return Extension switch + { + "PNG" => converter.BtexToPng(btex), + "TGA" => converter.BtexToTga(btex), + "DDS" => converter.BtexToDds(btex), + _ => btex + }; + } +} \ No newline at end of file diff --git a/Flagrum.Web/Features/AssetExplorer/Index.razor b/Flagrum.Web/Features/AssetExplorer/Index.razor index c33d6703..56b2c394 100644 --- a/Flagrum.Web/Features/AssetExplorer/Index.razor +++ b/Flagrum.Web/Features/AssetExplorer/Index.razor @@ -1,160 +1,202 @@ @page "/assets" +@using System.Timers @using Flagrum.Web.Features.AssetExplorer.Data -@using System.IO +@using Flagrum.Web.Persistence.Entities @inject AppStateService AppState @inject IWpfService WpfService +@inject SettingsService Settings +@inject FlagrumDbContext Context - - @*
*@ - @* arrow_back *@ - @* arrow_forward *@ - @* arrow_upward *@ - @*
*@ - @* *@ - @* *@ - @* *@ - @*
*@ - @*
*@ + + + + + + + +
+ @if (View == 1 && AppState.Node != null) + { + arrow_upward + } +
+ + + +
+ + + + +
- File System -
-
- @SelectedDirectory?.Item?.Name -
-
- @if (PreviewType == ExplorerItemType.Texture) + @if (View == 1) { - FFXV Texture File -
+ @if (View == 2) + { +
+ @SelectedDirectory?.Item?.Name +
+ } +
+ @(SelectedItem == null ? "No File Selected" : SelectedItem.DisplayName) +
- + @if (View == 1) + { + if (AppState.Node != null) + { + + } + else + { + + Game View will become available once Flagrum has finished indexing the game files + to allow for an improved browsing experience, searching, and filtering of all + game files (even those in archives).

+ This process may take several minutes depending on your system hardware. +
+ } + } + else + { + + }
-
- - - -
+ @if (View == 2) + { +
+ + + +
+ } +
- @if (PreviewType == ExplorerItemType.Texture) - { -
- -
- } - else if (PreviewType == ExplorerItemType.Material) - { - - } - else if (PreviewType == ExplorerItemType.Model) - { - - } - else - { -
-
- - Currently supports Textures (.btex) and Materials (.gmtl.gfxbin) - -
-
- } +
- - - Batch Export - cancel - - -
- Batch export currently only supports textures. -
- - -
-
- - Also export from subdirectories recursively -
-
-
-
- @code { - private string Directory { get; set; } - private string ImageName { get; set; } = "asset_preview.jpg"; - private ExplorerItemType PreviewType { get; set; } - private string MaterialPath { get; set; } - private string ModelPath { get; set; } - private ExplorerItemRow SelectedDirectory { get; set; } + private string _directory; + private Timer _timer; + + private int View { get; set; } = 1; + public ExplorerItemRow SelectedDirectory { get; set; } private ExplorerItemRow SelectedFile { get; set; } - private string Extension { get; set; } = "PNG"; - private Modal BatchExportModal { get; set; } - private bool IsRecursive { get; set; } - private bool IsLoading { get; set; } + private AssetExplorerItem SelectedItem { get; set; } - protected override void OnInitialized() + public bool IsLoading { get; set; } + private string LoadingMessage { get; set; } + + public AssetExplorerItem ContextItem { get; set; } + public AssetExplorerNode ContextNode { get; set; } + private BatchExportModal BatchExportModal { get; set; } + private RegenerateModal RegenerateModal { get; set; } + + private string CurrentPath { get; set; } + + private string Directory { - Directory = AppState.GetCurrentAssetExplorerPath(); + get => _directory; + set + { + _directory = value; + CurrentPath = value; + } } - public void SetActiveDirectory(string path) + protected override void OnInitialized() { - Directory = path; + Directory = AppState.GetCurrentAssetExplorerPath(); + + if (!System.IO.Directory.Exists(Directory)) + { + Directory = Environment.GetFolderPath(Environment.SpecialFolder.Desktop); + } + + if (AppState.Node == null) + { + _timer = new Timer(1000); + _timer.Elapsed += (_, _) => + { + if (AppState.Node != null) + { + _timer.Stop(); + InvokeAsync(StateHasChanged); + } + }; + _timer.Start(); + } + else + { + var nodeId = Context.GetInt(StateKey.CurrentAssetNode); + if (nodeId > 0) + { + AppState.Node = Context.AssetExplorerNodes + .FirstOrDefault(n => n.Id == nodeId); + CurrentPath = AppState.Node?.GetUri(Context); + } + } + + UpdateCurrentPath(); StateHasChanged(); } - public void SetPreviewMaterial(string path) + public void SetNode(AssetExplorerNode node) { - MaterialPath = path; - PreviewType = ExplorerItemType.Material; - StateHasChanged(); + if (node != null) + { + AppState.Node = Context.AssetExplorerNodes + .FirstOrDefault(n => n.Id == node.Id); + CurrentPath = node.GetUri(Context); + Context.SetInt(StateKey.CurrentAssetNode, node.Id); + + StateHasChanged(); + } } - public void SetPreviewModel(string path) + public void SetContextNode(AssetExplorerNode node) { - ModelPath = path; - PreviewType = ExplorerItemType.Model; + ContextNode = node; + ContextItem = AssetExplorerItem.FromNode(node, Context); StateHasChanged(); } - public void SetPreviewImage() + public void SetActiveDirectory(string path) { - // Jank to trick the UI into updating - ImageName = ImageName == "asset_preview.jpg" ? "Asset_Preview.jpg" : "asset_preview.jpg"; - - PreviewType = ExplorerItemType.Texture; + Directory = path; StateHasChanged(); } @@ -171,110 +213,54 @@ SelectedFile?.SetSelected(false); SelectedFile = item; SelectedFile.SetSelected(true); + SetSelectedItem(AssetExplorerItem.FromExplorerItem(item.Item)); } StateHasChanged(); } - private Task ExportPng() + public void SetSelectedItem(AssetExplorerItem item) { - return ExportTexture("png"); + SelectedItem = item; + CurrentPath = $"{AppState.Node.GetUri(Context)}/{item.Name}"; + StateHasChanged(); } - private Task ExportTga() + public void ClearSelectedItem() { - return ExportTexture("tga"); + SelectedItem = null; + CurrentPath = "data://"; + StateHasChanged(); } - private Task ExportDds() + private void OnViewChanged(int view) { - return ExportTexture("dds"); + View = view; + UpdateCurrentPath(); + StateHasChanged(); } - private async Task ExportTexture(string extension) + private void UpdateCurrentPath() { - await WpfService.OpenSaveFileDialogAsync( - SelectedFile.Item.Path.Split('\\').Last().Replace(".btex", $".{extension}"), - $"{extension.ToUpper()} Image|*.{extension}", path => - { - var btex = File.ReadAllBytes(SelectedFile.Item.Path); - var converter = new TextureConverter(); - var data = extension switch - { - "png" => converter.BtexToPng(btex), - "tga" => converter.BtexToTga(btex), - "dds" => converter.BtexToDds(btex), - _ => converter.BtexToJpg(btex) - }; - - File.WriteAllBytes(path, data); - }); + if (View == 1) + { + CurrentPath = AppState.Node == null ? "data://" : AppState.Node.GetUri(Context); + } + else + { + CurrentPath = Directory; + } } - private async Task ExportBatch() + public void SetLoading(bool state, string message = null) { - await WpfService.OpenFolderDialogAsync(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), folder => - { - InvokeAsync(() => - { - BatchExportModal.Close(); - IsLoading = true; - StateHasChanged(); - - var files = new List(); - - if (IsRecursive) - { - BuildFileListRecursively(SelectedDirectory.Item.Path, files); - } - else - { - files.AddRange(System.IO.Directory.GetFiles(SelectedDirectory.Item.Path, "*.btex")); - } - - var directories = files - .Select(f => - { - var relativePath = f.Replace(SelectedDirectory.Item.Path, "")[1..]; - var index = relativePath.LastIndexOf('\\'); - return index > 0 ? relativePath.Remove(index) : null; - }) - .Where(d => !string.IsNullOrWhiteSpace(d)) - .Distinct(); - - foreach (var directory in directories) - { - System.IO.Directory.CreateDirectory($"{folder}\\{directory}"); - } - - Parallel.ForEach(files, file => - { - var relativePath = file.Replace(SelectedDirectory.Item.Path, ""); - var outputPath = $"{folder}{relativePath.Replace(".btex", $".{Extension.ToLower()}")}"; - var btex = File.ReadAllBytes(file); - var converter = new TextureConverter(); - var data = Extension switch - { - "PNG" => converter.BtexToPng(btex), - "TGA" => converter.BtexToTga(btex), - _ => converter.BtexToDds(btex) - }; - - File.WriteAllBytes(outputPath, data); - }); - - IsLoading = false; - StateHasChanged(); - }); - }); + LoadingMessage = message; + IsLoading = state; + StateHasChanged(); } - private void BuildFileListRecursively(string directory, List files) + public void CallStateHasChanged() { - files.AddRange(System.IO.Directory.GetFiles(directory, "*.btex")); - foreach (var subdirectory in System.IO.Directory.EnumerateDirectories(directory)) - { - BuildFileListRecursively(subdirectory, files); - } + StateHasChanged(); } } \ No newline at end of file diff --git a/Flagrum.Web/Features/AssetExplorer/Previews/MaterialPreview.razor b/Flagrum.Web/Features/AssetExplorer/Previews/MaterialPreview.razor index ecf0b764..58cd51d3 100644 --- a/Flagrum.Web/Features/AssetExplorer/Previews/MaterialPreview.razor +++ b/Flagrum.Web/Features/AssetExplorer/Previews/MaterialPreview.razor @@ -1,4 +1,5 @@ -@using Flagrum.Core.Gfxbin.Gmtl +@using Flagrum.Web.Features.AssetExplorer.Data +@using Flagrum.Core.Gfxbin.Gmtl
@BaseMaterial
@@ -44,10 +45,10 @@ @code { - private string _previousPath; + private AssetExplorerItem _previousItem; [Parameter] - public string Path { get; set; } + public AssetExplorerItem Item { get; set; } private string BaseMaterial { get; set; } private IEnumerable<(string Name, float[] Values)> MaterialInputs { get; set; } @@ -56,12 +57,12 @@ protected override void OnParametersSet() { - if (Path == _previousPath) + if (_previousItem == Item) { return; } - var reader = new MaterialReader(Path); + var reader = new MaterialReader(Item.Data()); var material = reader.Read(); BaseMaterial = material.Interfaces[0].Name; @@ -77,6 +78,7 @@ MaterialInputs = inputs; TextureInputs = textures; + StateHasChanged(); } private void ShowPath(string path) diff --git a/Flagrum.Web/Features/AssetExplorer/Previews/ModelPreview.razor b/Flagrum.Web/Features/AssetExplorer/Previews/ModelPreview.razor index 4d41b5bf..aa68e00b 100644 --- a/Flagrum.Web/Features/AssetExplorer/Previews/ModelPreview.razor +++ b/Flagrum.Web/Features/AssetExplorer/Previews/ModelPreview.razor @@ -1,8 +1,7 @@ @using Blazor.Extensions +@using Flagrum.Web.Features.AssetExplorer.Data @using Blazor.Extensions.Canvas.WebGL -@using Flagrum.Core.Gfxbin.Gmdl @using Flagrum.Core.Gfxbin.Gmdl.Components -@using System.IO @implements IDisposable @@ -10,7 +9,7 @@ @code { [Parameter] - public string Path { get; set; } + public AssetExplorerItem Item { get; set; } private WebGLContext Context { get; set; } private BECanvasComponent Canvas { get; set; } @@ -19,9 +18,9 @@ protected override void OnInitialized() { - var gpu = Path.Replace(".gmdl.gfxbin", ".gpubin"); - var reader = new ModelReader(File.ReadAllBytes(Path), File.ReadAllBytes(gpu)); - Model = reader.Read(); + //var gpu = Path.Replace(".gmdl.gfxbin", ".gpubin"); + //var reader = new ModelReader(File.ReadAllBytes(Path), File.ReadAllBytes(gpu)); + //Model = reader.Read(); } protected override async Task OnAfterRenderAsync(bool firstRender) diff --git a/Flagrum.Web/Features/AssetExplorer/Previews/Preview.razor b/Flagrum.Web/Features/AssetExplorer/Previews/Preview.razor new file mode 100644 index 00000000..b3724a4c --- /dev/null +++ b/Flagrum.Web/Features/AssetExplorer/Previews/Preview.razor @@ -0,0 +1,29 @@ +@using Flagrum.Web.Features.AssetExplorer.Data +@if (Item?.Type == ExplorerItemType.Texture) +{ + +} +else if (Item?.Type == ExplorerItemType.Material) +{ + +} +else if (Item?.Type == ExplorerItemType.Model) +{ + +} +else +{ +
+
+ + Currently supports Textures (.btex) and Materials (.gmtl.gfxbin) + +
+
+} + +@code +{ + [Parameter] + public AssetExplorerItem Item { get; set; } +} \ No newline at end of file diff --git a/Flagrum.Web/Features/AssetExplorer/Previews/TexturePreview.razor b/Flagrum.Web/Features/AssetExplorer/Previews/TexturePreview.razor new file mode 100644 index 00000000..f5757307 --- /dev/null +++ b/Flagrum.Web/Features/AssetExplorer/Previews/TexturePreview.razor @@ -0,0 +1,34 @@ +@using Flagrum.Web.Features.AssetExplorer.Data +@using System.IO +@using Flagrum.Core.Utilities +
+ +
+ +@code +{ + private AssetExplorerItem _previousItem; + + [Parameter] + public AssetExplorerItem Item { get; set; } + + private string ImageName { get; set; } = "asset_preview.jpg"; + + protected override void OnParametersSet() + { + if (_previousItem != Item) + { + _previousItem = Item; + + // Convert to jpeg for display + var converter = new TextureConverter(); + var jpegBytes = converter.BtexToJpg(Item.Data()); + File.WriteAllBytes($"{IOHelper.GetWebRoot()}\\images\\asset_preview.jpg", jpegBytes); + + // Jank to trick the UI into updating + ImageName = ImageName == "asset_preview.jpg" ? "Asset_Preview.jpg" : "asset_preview.jpg"; + + StateHasChanged(); + } + } +} \ No newline at end of file diff --git a/Flagrum.Web/Features/AssetExplorer/RegenerateModal.razor b/Flagrum.Web/Features/AssetExplorer/RegenerateModal.razor new file mode 100644 index 00000000..d0cfaf7b --- /dev/null +++ b/Flagrum.Web/Features/AssetExplorer/RegenerateModal.razor @@ -0,0 +1,51 @@ +@using Microsoft.EntityFrameworkCore +@using Microsoft.EntityFrameworkCore.Internal +@inject AppStateService AppState +@inject UriMapper Mapper +@inject FlagrumDbContext Context + + + + Rebuild File Index + cancel + + +
+ + If your game files have lost sync with Flagrum's file index (such as if you have added extra + content to the game or installed mods from other sources), you can regenerate the file index + to repair the Asset Explorer's Game View. This may take several minutes to complete. + +
+
+
+ +@code +{ + [CascadingParameter] + public Index Parent { get; set; } + + private Modal Modal { get; set; } + + public void Open() => Modal.Open(); + + private async Task OnClick() + { + AppState.Node = null; + Parent.ClearSelectedItem(); + Parent.CallStateHasChanged(); + Modal.Close(); + + await Task.Run(() => + { + Mapper.RegenerateMap(); + Context.GetDependencies().StateManager.ResetState(); + + AppState.Node = Context.AssetExplorerNodes + .FirstOrDefault(n => n.Id == 1); + + InvokeAsync(() => Parent.CallStateHasChanged()); + }); + } +} diff --git a/Flagrum.Web/Features/AssetExplorer/VirtualExplorerRow.razor b/Flagrum.Web/Features/AssetExplorer/VirtualExplorerRow.razor new file mode 100644 index 00000000..0020f862 --- /dev/null +++ b/Flagrum.Web/Features/AssetExplorer/VirtualExplorerRow.razor @@ -0,0 +1,89 @@ +@using Flagrum.Web.Persistence +@using Flagrum.Web.Persistence.Entities +@using Flagrum.Web.Features.AssetExplorer.Data +@inject FlagrumDbContext Context +@inject SettingsService Settings + +
+ @Icon(Type(Node)) + @DisplayName(Node) +
+ +@code +{ + [CascadingParameter] + public Index Parent { get; set; } + + [Parameter] + public AssetExplorerNode Node { get; set; } + + private void OnClick(AssetExplorerNode node) + { + if (Context.AssetExplorerNodes.Any(n => n.ParentId == node.Id)) + { + Parent.SetNode(node); + } + else + { + Parent.SetSelectedItem(AssetExplorerItem.FromNode(node, Context)); + } + } + + private string DisplayName(AssetExplorerNode node) + { + if (Type(node) == ExplorerItemType.Directory || node.Name.StartsWith("CRAF")) + { + return node.Name; + } + + var extension = node.Name[node.Name.LastIndexOf('.')..].ToLower(); + var trueExtension = extension switch + { + ".tif" or ".tga" or ".png" or ".dds" or ".exr" => ".btex", + ".gmtl" => ".gmtl.gfxbin", + ".gmdl" => ".gmdl.gfxbin", + _ => extension + }; + + return node.Name[..node.Name.LastIndexOf('.')] + trueExtension; + } + + private string Icon(ExplorerItemType type) + { + return type switch + { + ExplorerItemType.Directory => "folder", + ExplorerItemType.Material => "blur_circular", + ExplorerItemType.Texture => "gradient", + ExplorerItemType.Model => "view_in_ar", + _ => "insert_drive_file" + }; + } + + private ExplorerItemType Type(AssetExplorerNode node) + { + if (Context.AssetExplorerNodes.Any(n => n.ParentId == node.Id)) + { + return ExplorerItemType.Directory; + } + + var tokens = node.Name.Split('.').Select(t => t.ToLower()).ToArray(); + var extension = tokens[^1] == "gfxbin" + ? string.Join('.', tokens[^2..]) + : tokens[^1]; + return ExtensionToType(extension); + } + + private ExplorerItemType ExtensionToType(string extension) + { + return extension?.ToLower() switch + { + "btex" or "tif" or "tga" or "png" or "dds" or "exr" => ExplorerItemType.Texture, + "gmtl.gfxbin" or "gmtl" => ExplorerItemType.Material, + //"gmdl.gfxbin" => ExplorerItemType.Model, + null or "" => ExplorerItemType.Directory, + _ => ExplorerItemType.Unsupported + }; + } +} \ No newline at end of file diff --git a/Flagrum.Web/Features/AssetExplorer/VirtualExplorerView.razor b/Flagrum.Web/Features/AssetExplorer/VirtualExplorerView.razor new file mode 100644 index 00000000..d6ea13df --- /dev/null +++ b/Flagrum.Web/Features/AssetExplorer/VirtualExplorerView.razor @@ -0,0 +1,23 @@ +@using Flagrum.Web.Persistence +@using Flagrum.Web.Persistence.Entities +@inject SettingsService Settings +@inject FlagrumDbContext Context + +@foreach (var node in Context.AssetExplorerNodes + .Where(n => n.ParentId == Node.Id) + .OrderByDescending(n => n.Children.Any()) + .ThenBy(n => n.Name)) +{ + + + +} + +@code +{ + [CascadingParameter] + public Index Parent { get; set; } + + [Parameter] + public AssetExplorerNode Node { get; set; } +} \ No newline at end of file diff --git a/Flagrum.Web/Features/AssetExplorer/_Imports.razor b/Flagrum.Web/Features/AssetExplorer/_Imports.razor index 48d31774..de1b6795 100644 --- a/Flagrum.Web/Features/AssetExplorer/_Imports.razor +++ b/Flagrum.Web/Features/AssetExplorer/_Imports.razor @@ -1 +1,2 @@ -@using Flagrum.Web.Features.AssetExplorer.Previews \ No newline at end of file +@using Flagrum.Web.Features.AssetExplorer.Previews +@using Flagrum.Web.Features.AssetExplorer.Export \ No newline at end of file diff --git a/Flagrum.Web/Features/EarcMods/EarcModCard.razor b/Flagrum.Web/Features/EarcMods/EarcModCard.razor new file mode 100644 index 00000000..9607e521 --- /dev/null +++ b/Flagrum.Web/Features/EarcMods/EarcModCard.razor @@ -0,0 +1,28 @@ +
+
+
+ more_vert +
+
+
+ Core Gameplay Mod + + Description line 1
+ Description line 2
+ Description line 3 +
+
+ @*
*@ + @*
*@ +
+ +@code +{ + [Parameter] + public bool IsDisabled { get; set; } +} diff --git a/Flagrum.Web/Features/EarcMods/Index.razor b/Flagrum.Web/Features/EarcMods/Index.razor new file mode 100644 index 00000000..0560608f --- /dev/null +++ b/Flagrum.Web/Features/EarcMods/Index.razor @@ -0,0 +1,35 @@ +@page "/earc" +@using Flagrum.Core.Utilities +@using System.IO + +
+
+ +

Active Mods

+
+ @for (var i = 0; i < 4; i++) + { + + } +
+ +

Disabled Mods

+
+ @for (var i = 0; i < 2; i++) + { + + } +
+ +@code +{ + protected override void OnInitialized() + { + //var defaultPreviewPath = $"{IOHelper.GetExecutingDirectory()}\\Resources\\preview.png"; + var defaultPreviewPath = @"C:\Users\Kieran\Desktop\Cursed\fabulous.jpg"; + var currentPreviewPath = $"{IOHelper.GetWebRoot()}\\images\\current_preview.png"; + File.Copy(defaultPreviewPath, currentPreviewPath, true); + } +} \ No newline at end of file diff --git a/Flagrum.Web/Features/FileInspector/Index.razor b/Flagrum.Web/Features/FileInspector/Index.razor deleted file mode 100644 index 14b2e1e0..00000000 --- a/Flagrum.Web/Features/FileInspector/Index.razor +++ /dev/null @@ -1,61 +0,0 @@ -@page "/inspector" -@using System.IO -@using Flagrum.Core.Gfxbin.Gmdl -@using Flagrum.Core.Gfxbin.Gmtl -@inject IWpfService Wpf - -
- -
- Select file to inspect -
- - @if (Object != null) - { -
- -
- } - -
- -@code -{ - private object Object { get; set; } - - private async Task BrowseClicked() - { - await Wpf.OpenFileDialogAsync( - "Game Model (*.gmdl.gfxbin)|*.gmdl.gfxbin|" + - "Game Material (*.gmtl.gfxbin)|*.gmtl.gfxbin", // + - //"Asset Archive (*.earc)|*.earc|" + - //"FFXV Mod (*.ffxvbinmod)|*.ffxvbinmod", - path => - { - if (path.EndsWith(".gmdl.gfxbin")) - { - var gpubinPath = path.Replace(".gmdl.gfxbin", ".gpubin"); - // TODO: Show an error to the user if the gpubin is missing - if (!File.Exists(gpubinPath)) - { - return; - } - - var modelReader = new ModelReader(File.ReadAllBytes(path), File.ReadAllBytes(gpubinPath)); - Object = modelReader.Read(); - } - else if (path.EndsWith(".gmtl.gfxbin")) - { - var materialReader = new MaterialReader(path); - Object = materialReader.Read(); - } - else if (path.EndsWith(".earc") || path.EndsWith(".ffxvbinmod")) - { - // TODO: Implement something here - } - - InvokeAsync(StateHasChanged); - }); - } -} \ No newline at end of file diff --git a/Flagrum.Web/Features/FileInspector/ObjectInspector.razor b/Flagrum.Web/Features/FileInspector/ObjectInspector.razor deleted file mode 100644 index a21b177d..00000000 --- a/Flagrum.Web/Features/FileInspector/ObjectInspector.razor +++ /dev/null @@ -1,93 +0,0 @@ -@using System.Reflection -@using System.Collections - - @foreach (var property in Object.GetType().GetProperties()) - { - var isExpandable = !property.PropertyType.IsPrimitive && property.PropertyType != typeof(string); - var isEnumerable = property.PropertyType.GetInterfaces().Any(i => i == typeof(IEnumerable)) && property.PropertyType != typeof(string); - - object value; - if (property.PropertyType.GetTypeInfo().IsArray) - { - continue; - } - value = property.GetValue(Object); - - var count = 0; - if (isEnumerable) - { - var iterator = ((IEnumerable)value).GetEnumerator(); - using (iterator as IDisposable) - { - while (iterator.MoveNext()) - { - count++; - } - } - } - - - - - - - - @if (isExpandable && IsExpanded) - { - if (isEnumerable && !value.GetType().GetTypeInfo().IsArray) - { - - - - - } - else if (value != null && !value.GetType().GetTypeInfo().IsArray) - { - - - - - } - } - } -
- @if (isExpandable) - { - @(IsExpanded ? "indeterminate_check_box" : "add_box") - } - - @property.Name - - @if (isEnumerable) - { - @count items - } - else - { - @value?.ToString() - } -
- @foreach (var item in (IEnumerable)value) - { - if (!item.GetType().GetTypeInfo().IsArray) - { - - } - } -
- -
- -@code -{ - [Parameter] - public object Object { get; set; } - - [Parameter] - public bool IsExpanded { get; set; } - - private void Toggle() - { - IsExpanded = !IsExpanded; - } -} \ No newline at end of file diff --git a/Flagrum.Web/Features/ModLibrary/Index.razor b/Flagrum.Web/Features/ModLibrary/Index.razor index 45d74caf..c8650dea 100644 --- a/Flagrum.Web/Features/ModLibrary/Index.razor +++ b/Flagrum.Web/Features/ModLibrary/Index.razor @@ -1,8 +1,5 @@ @page "/" @using Flagrum.Core.Archive -@using Flagrum.Core.Utilities -@using System.Collections.Concurrent -@using System.IO @inject NavigationManager Navigation @inject SettingsService Settings @@ -128,88 +125,12 @@ private List DefaultReplacements { get; set; } - protected override async void OnInitialized() + protected override void OnInitialized() { DefaultReplacements = ModelReplacementPresets.GetDefaultReplacements(); - if (!AppState.IsModListInitialized) - { - await Task.Run(() => - { - var binmodList = ModlistEntry.FromFile(Settings.BinmodListPath); - var localMods = Directory.GetFiles(Settings.ModDirectory, "*.ffxvbinmod", SearchOption.TopDirectoryOnly); - IEnumerable allMods; - - if (Directory.Exists(Settings.WorkshopDirectory)) - { - var workshopMods = Directory.GetFiles(Settings.WorkshopDirectory, "*.ffxvbinmod", SearchOption.AllDirectories); - allMods = localMods.Union(workshopMods); - } - else - { - allMods = localMods; - } - - var mods = new ConcurrentBag(); - - Parallel.ForEach(allMods, file => - { - using var unpacker = new Unpacker(file); - var modmetaBytes = unpacker.UnpackFileByQuery("index.modmeta", out _); - var mod = Binmod.FromModmetaBytes(modmetaBytes, BinmodTypeHelper, Logger); - var previewBytes = unpacker.UnpackFileByQuery("$preview.png.bin", out _); - - var binmodListing = binmodList.FirstOrDefault(e => file.Contains(e.Path.Replace('/', '\\'))); - - if (mod == null) - { - Logger.LogWarning($"Could not read modmeta from {file}"); - return; - } - - if (binmodListing == null) - { - Logger.LogWarning($"Could not find binmod.list entry for {file}"); - return; - } - - mod.Description = mod.Description?.Replace("\\n", "\n"); - mod.GameMenuDescription = mod.GameMenuDescription?.Replace("\\n", "\n"); - mod.IsWorkshopMod = binmodListing.IsWorkshopMod; - mod.Index = binmodListing.Index; - mod.IsApplyToGame = binmodListing.IsEnabled; - mod.Path = file; - File.WriteAllBytes($"{IOHelper.GetWebRoot()}\\images\\{mod.Uuid}.png", previewBytes); - - mods.Add(mod); - }); - - AppState.Mods = mods.ToList(); - var paths = AppState.Mods.Select(m => m.Path); - AppState.UnmanagedEntries = binmodList.Where(e => !paths.Any(p => p.Contains(e.Path.Replace('/', '\\')))).ToList(); - }); - - ClearOldImages(); - AppState.IsModListInitialized = true; - } - StateHasChanged(); } - private async void ClearOldImages() - { - await Task.Run(() => - { - var exceptions = AppState.Mods.Select(m => $"{IOHelper.GetWebRoot()}\\images\\{m.Uuid}.png"); - foreach (var image in Directory.EnumerateFiles($"{IOHelper.GetWebRoot()}\\images")) - { - if (!exceptions.Contains(image)) - { - File.Delete(image); - } - } - }); - } - private void FilterType(int type) { AppState.ActiveModTypeFilter = type; diff --git a/Flagrum.Web/Flagrum.Web.csproj b/Flagrum.Web/Flagrum.Web.csproj index a7a2b675..3ade94e6 100644 --- a/Flagrum.Web/Flagrum.Web.csproj +++ b/Flagrum.Web/Flagrum.Web.csproj @@ -6,10 +6,16 @@ + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + @@ -26,37 +32,8 @@ <_ContentIncludedByDefault Remove="Layout\MainLayout.razor" /> <_ContentIncludedByDefault Remove="Layout\NodeExplorer.razor" /> <_ContentIncludedByDefault Remove="Layout\TypeTreeComponent.razor" /> - - - - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - + <_ContentIncludedByDefault Remove="Features\FileInspector\Index.razor" /> + <_ContentIncludedByDefault Remove="Features\FileInspector\ObjectInspector.razor" /> diff --git a/Flagrum.Web/Layout/MainLayout.razor b/Flagrum.Web/Layout/MainLayout.razor index 8919601b..514f2678 100644 --- a/Flagrum.Web/Layout/MainLayout.razor +++ b/Flagrum.Web/Layout/MainLayout.razor @@ -8,9 +8,20 @@ {
- - @Body - + @if (AppState.Node == null) + { +
+ + + Indexing game files, this may take several minutes, but will only need to happen once + +
+ } +
+ + @Body + +
} else diff --git a/Flagrum.Web/Layout/MainMenu.razor b/Flagrum.Web/Layout/MainMenu.razor index fb1317be..379b94f1 100644 --- a/Flagrum.Web/Layout/MainMenu.razor +++ b/Flagrum.Web/Layout/MainMenu.razor @@ -7,9 +7,10 @@ - @* *@ + @* *@ @* *@
+ @@ -29,6 +30,20 @@

Flagrum has been updated to @Version!

+

Flagrum 1.1.5

+
Asset Explorer Game View
+
    +
  • Allows for browsing game files without extraction (including preview of supported types)
  • +
  • Added "Export with Dependencies" option that exports models with all textures and materials
  • +
  • Added basic "Export" and "Export Folder" options for all file types
  • +
+
Modding
+
    +
  • Fixed a bug where Model Replacement Mods were not rigging correctly
  • +
+
+ +

Flagrum 1.1.4

Modding
    @@ -36,19 +51,19 @@
  • Support for custom normals from Flagrum Blender 1.0.4
- +

Flagrum 1.1.3

Modding
  • - Support for increased - + Support for increased + on meshes with materials that support it
- +

Flagrum 1.1.2

Modding
diff --git a/Flagrum.Web/Persistence/Entities/ArchiveLocation.cs b/Flagrum.Web/Persistence/Entities/ArchiveLocation.cs new file mode 100644 index 00000000..48e17cd6 --- /dev/null +++ b/Flagrum.Web/Persistence/Entities/ArchiveLocation.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace Flagrum.Web.Persistence.Entities; + +public class ArchiveLocation +{ + public int Id { get; set; } + public string Path { get; set; } + + public ICollection AssetUris { get; set; } +} \ No newline at end of file diff --git a/Flagrum.Web/Persistence/Entities/AssetExplorerNode.cs b/Flagrum.Web/Persistence/Entities/AssetExplorerNode.cs new file mode 100644 index 00000000..fe23a416 --- /dev/null +++ b/Flagrum.Web/Persistence/Entities/AssetExplorerNode.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Flagrum.Web.Services; + +namespace Flagrum.Web.Persistence.Entities; + +public class AssetExplorerNode +{ + public int Id { get; set; } + + public string Name { get; set; } + + public int? ParentId { get; set; } + public AssetExplorerNode Parent { get; set; } + + public ICollection Children { get; set; } = new HashSet(); + + public void Traverse(FlagrumDbContext context, Action visitor) + { + visitor(this); + foreach (var node in context.AssetExplorerNodes.Where(n => n.ParentId == Id)) + { + node.Traverse(context, visitor); + } + } + + public void TraverseDescending(FlagrumDbContext context, Action visitor) + { + visitor(this); + + if (ParentId != null) + { + Parent = context.AssetExplorerNodes.Find(ParentId); + Parent?.TraverseDescending(context, visitor); + } + } + + public string GetUri(FlagrumDbContext context) + { + var uri = ""; + TraverseDescending(context, node => uri = node.Name + (uri.Length > 0 && !node.Name.EndsWith('/') ? "/" : "") + uri); + return uri; + } + + public string GetLocation(FlagrumDbContext context, SettingsService settings) + { + var uri = GetUri(context); + return settings.GameDataDirectory + "\\" + context.AssetUris + .Where(a => a.Uri == uri) + .Select(a => a.ArchiveLocation.Path) + .FirstOrDefault(); + } +} \ No newline at end of file diff --git a/Flagrum.Web/Persistence/Entities/AssetUri.cs b/Flagrum.Web/Persistence/Entities/AssetUri.cs new file mode 100644 index 00000000..eb70a23a --- /dev/null +++ b/Flagrum.Web/Persistence/Entities/AssetUri.cs @@ -0,0 +1,28 @@ +using System.ComponentModel.DataAnnotations; +using System.Linq; +using Flagrum.Core.Archive; +using Flagrum.Web.Services; + +namespace Flagrum.Web.Persistence.Entities; + +public class AssetUri +{ + [Key] + public string Uri { get; set; } + + public int ArchiveLocationId { get; set; } + public ArchiveLocation ArchiveLocation { get; set; } +} + +public static class AssetUriExtensions +{ + public static byte[] GetFileByUri(this FlagrumDbContext context, string uri) + { + var location = context.Settings.GameDataDirectory + "\\" + context.AssetUris + .Where(a => a.Uri == uri) + .Select(a => a.ArchiveLocation.Path) + .FirstOrDefault(); + + return Unpacker.GetFileByLocation(location, uri); + } +} \ No newline at end of file diff --git a/Flagrum.Web/Persistence/Entities/FlagrumMod.cs b/Flagrum.Web/Persistence/Entities/FlagrumMod.cs new file mode 100644 index 00000000..98f2fbaf --- /dev/null +++ b/Flagrum.Web/Persistence/Entities/FlagrumMod.cs @@ -0,0 +1,9 @@ +namespace Flagrum.Web.Persistence.Entities; + +public class FlagrumMod +{ + public int Id { get; set; } + + public string Name { get; set; } + public string ImagePath { get; set; } +} \ No newline at end of file diff --git a/Flagrum.Web/Persistence/Entities/StatePair.cs b/Flagrum.Web/Persistence/Entities/StatePair.cs new file mode 100644 index 00000000..5d34314b --- /dev/null +++ b/Flagrum.Web/Persistence/Entities/StatePair.cs @@ -0,0 +1,52 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.Linq; + +namespace Flagrum.Web.Persistence.Entities; + +public enum StateKey +{ + CurrentAssetNode +} + +public class StatePair +{ + [Key] public StateKey Key { get; set; } + + public string Value { get; set; } +} + +public static class StatePairExtensions +{ + public static string GetString(this FlagrumDbContext context, StateKey key) + { + return context.StatePairs.FirstOrDefault(p => p.Key == key)?.Value; + } + + public static int GetInt(this FlagrumDbContext context, StateKey key) + { + var value = context.StatePairs.FirstOrDefault(p => p.Key == key); + return value == null ? -1 : Convert.ToInt32(value.Value); + } + + public static void SetString(this FlagrumDbContext context, StateKey key, string value) + { + var pair = context.StatePairs.FirstOrDefault(p => p.Key == key); + if (pair == null) + { + pair = new StatePair {Key = key, Value = value}; + context.Add(pair); + } + else + { + pair.Value = value; + } + + context.SaveChanges(); + } + + public static void SetInt(this FlagrumDbContext context, StateKey key, int value) + { + SetString(context, key, value.ToString()); + } +} \ No newline at end of file diff --git a/Flagrum.Web/Persistence/FlagrumDbContext.cs b/Flagrum.Web/Persistence/FlagrumDbContext.cs new file mode 100644 index 00000000..046dde10 --- /dev/null +++ b/Flagrum.Web/Persistence/FlagrumDbContext.cs @@ -0,0 +1,41 @@ +using System; +using System.Reflection; +using Flagrum.Web.Persistence.Entities; +using Flagrum.Web.Services; +using Microsoft.EntityFrameworkCore; + +namespace Flagrum.Web.Persistence; + +public class FlagrumDbContext : DbContext +{ + public FlagrumDbContext() { } + + public FlagrumDbContext(SettingsService settings) + { + Settings = settings; + } + + public SettingsService Settings { get; } + + public DbSet AssetExplorerNodes { get; set; } + public DbSet ArchiveLocations { get; set; } + public DbSet AssetUris { get; set; } + public DbSet StatePairs { get; set; } + + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + var databasePath = + $@"{Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)}\Flagrum\flagrum.db"; + + optionsBuilder.UseSqlite($"Data Source={databasePath};", options => { options.CommandTimeout(90); }); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.ApplyConfigurationsFromAssembly( + Assembly.GetAssembly(typeof(FlagrumDbContext)) + ?? throw new InvalidOperationException("Assembly cannot be null")); + } +} \ No newline at end of file diff --git a/Flagrum.Web/Persistence/Migrations/20220129040735_Initial.Designer.cs b/Flagrum.Web/Persistence/Migrations/20220129040735_Initial.Designer.cs new file mode 100644 index 00000000..fd221061 --- /dev/null +++ b/Flagrum.Web/Persistence/Migrations/20220129040735_Initial.Designer.cs @@ -0,0 +1,102 @@ +// +using System; +using Flagrum.Web.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Flagrum.Web.Persistence.Migrations +{ + [DbContext(typeof(FlagrumDbContext))] + [Migration("20220129040735_Initial")] + partial class Initial + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "6.0.1"); + + modelBuilder.Entity("Flagrum.Web.Persistence.Entities.ArchiveLocation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ArchiveLocations"); + }); + + modelBuilder.Entity("Flagrum.Web.Persistence.Entities.AssetExplorerNode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("ParentId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ParentId"); + + b.ToTable("AssetExplorerNodes"); + }); + + modelBuilder.Entity("Flagrum.Web.Persistence.Entities.AssetUri", b => + { + b.Property("Uri") + .HasColumnType("TEXT"); + + b.Property("ArchiveLocationId") + .HasColumnType("INTEGER"); + + b.HasKey("Uri"); + + b.HasIndex("ArchiveLocationId"); + + b.ToTable("AssetUris"); + }); + + modelBuilder.Entity("Flagrum.Web.Persistence.Entities.AssetExplorerNode", b => + { + b.HasOne("Flagrum.Web.Persistence.Entities.AssetExplorerNode", "Parent") + .WithMany("Children") + .HasForeignKey("ParentId"); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("Flagrum.Web.Persistence.Entities.AssetUri", b => + { + b.HasOne("Flagrum.Web.Persistence.Entities.ArchiveLocation", "ArchiveLocation") + .WithMany("AssetUris") + .HasForeignKey("ArchiveLocationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ArchiveLocation"); + }); + + modelBuilder.Entity("Flagrum.Web.Persistence.Entities.ArchiveLocation", b => + { + b.Navigation("AssetUris"); + }); + + modelBuilder.Entity("Flagrum.Web.Persistence.Entities.AssetExplorerNode", b => + { + b.Navigation("Children"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Flagrum.Web/Persistence/Migrations/20220129040735_Initial.cs b/Flagrum.Web/Persistence/Migrations/20220129040735_Initial.cs new file mode 100644 index 00000000..d0a68da9 --- /dev/null +++ b/Flagrum.Web/Persistence/Migrations/20220129040735_Initial.cs @@ -0,0 +1,84 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Flagrum.Web.Persistence.Migrations +{ + public partial class Initial : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ArchiveLocations", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Path = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ArchiveLocations", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AssetExplorerNodes", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Name = table.Column(type: "TEXT", nullable: true), + ParentId = table.Column(type: "INTEGER", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AssetExplorerNodes", x => x.Id); + table.ForeignKey( + name: "FK_AssetExplorerNodes_AssetExplorerNodes_ParentId", + column: x => x.ParentId, + principalTable: "AssetExplorerNodes", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "AssetUris", + columns: table => new + { + Uri = table.Column(type: "TEXT", nullable: false), + ArchiveLocationId = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AssetUris", x => x.Uri); + table.ForeignKey( + name: "FK_AssetUris_ArchiveLocations_ArchiveLocationId", + column: x => x.ArchiveLocationId, + principalTable: "ArchiveLocations", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_AssetExplorerNodes_ParentId", + table: "AssetExplorerNodes", + column: "ParentId"); + + migrationBuilder.CreateIndex( + name: "IX_AssetUris_ArchiveLocationId", + table: "AssetUris", + column: "ArchiveLocationId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AssetExplorerNodes"); + + migrationBuilder.DropTable( + name: "AssetUris"); + + migrationBuilder.DropTable( + name: "ArchiveLocations"); + } + } +} diff --git a/Flagrum.Web/Persistence/Migrations/20220202142725_StatePairs.Designer.cs b/Flagrum.Web/Persistence/Migrations/20220202142725_StatePairs.Designer.cs new file mode 100644 index 00000000..456a2636 --- /dev/null +++ b/Flagrum.Web/Persistence/Migrations/20220202142725_StatePairs.Designer.cs @@ -0,0 +1,115 @@ +// +using System; +using Flagrum.Web.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Flagrum.Web.Persistence.Migrations +{ + [DbContext(typeof(FlagrumDbContext))] + [Migration("20220202142725_StatePairs")] + partial class StatePairs + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "6.0.1"); + + modelBuilder.Entity("Flagrum.Web.Persistence.Entities.ArchiveLocation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ArchiveLocations"); + }); + + modelBuilder.Entity("Flagrum.Web.Persistence.Entities.AssetExplorerNode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("ParentId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ParentId"); + + b.ToTable("AssetExplorerNodes"); + }); + + modelBuilder.Entity("Flagrum.Web.Persistence.Entities.AssetUri", b => + { + b.Property("Uri") + .HasColumnType("TEXT"); + + b.Property("ArchiveLocationId") + .HasColumnType("INTEGER"); + + b.HasKey("Uri"); + + b.HasIndex("ArchiveLocationId"); + + b.ToTable("AssetUris"); + }); + + modelBuilder.Entity("Flagrum.Web.Persistence.Entities.StatePair", b => + { + b.Property("Key") + .HasColumnType("INTEGER"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Key"); + + b.ToTable("StatePairs"); + }); + + modelBuilder.Entity("Flagrum.Web.Persistence.Entities.AssetExplorerNode", b => + { + b.HasOne("Flagrum.Web.Persistence.Entities.AssetExplorerNode", "Parent") + .WithMany("Children") + .HasForeignKey("ParentId"); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("Flagrum.Web.Persistence.Entities.AssetUri", b => + { + b.HasOne("Flagrum.Web.Persistence.Entities.ArchiveLocation", "ArchiveLocation") + .WithMany("AssetUris") + .HasForeignKey("ArchiveLocationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ArchiveLocation"); + }); + + modelBuilder.Entity("Flagrum.Web.Persistence.Entities.ArchiveLocation", b => + { + b.Navigation("AssetUris"); + }); + + modelBuilder.Entity("Flagrum.Web.Persistence.Entities.AssetExplorerNode", b => + { + b.Navigation("Children"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Flagrum.Web/Persistence/Migrations/20220202142725_StatePairs.cs b/Flagrum.Web/Persistence/Migrations/20220202142725_StatePairs.cs new file mode 100644 index 00000000..8fbb24b6 --- /dev/null +++ b/Flagrum.Web/Persistence/Migrations/20220202142725_StatePairs.cs @@ -0,0 +1,30 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Flagrum.Web.Persistence.Migrations +{ + public partial class StatePairs : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "StatePairs", + columns: table => new + { + Key = table.Column(type: "INTEGER", nullable: false), + Value = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_StatePairs", x => x.Key); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "StatePairs"); + } + } +} diff --git a/Flagrum.Web/Persistence/Migrations/FlagrumDbContextModelSnapshot.cs b/Flagrum.Web/Persistence/Migrations/FlagrumDbContextModelSnapshot.cs new file mode 100644 index 00000000..db7cb45b --- /dev/null +++ b/Flagrum.Web/Persistence/Migrations/FlagrumDbContextModelSnapshot.cs @@ -0,0 +1,113 @@ +// +using System; +using Flagrum.Web.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Flagrum.Web.Persistence.Migrations +{ + [DbContext(typeof(FlagrumDbContext))] + partial class FlagrumDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "6.0.1"); + + modelBuilder.Entity("Flagrum.Web.Persistence.Entities.ArchiveLocation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ArchiveLocations"); + }); + + modelBuilder.Entity("Flagrum.Web.Persistence.Entities.AssetExplorerNode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("ParentId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ParentId"); + + b.ToTable("AssetExplorerNodes"); + }); + + modelBuilder.Entity("Flagrum.Web.Persistence.Entities.AssetUri", b => + { + b.Property("Uri") + .HasColumnType("TEXT"); + + b.Property("ArchiveLocationId") + .HasColumnType("INTEGER"); + + b.HasKey("Uri"); + + b.HasIndex("ArchiveLocationId"); + + b.ToTable("AssetUris"); + }); + + modelBuilder.Entity("Flagrum.Web.Persistence.Entities.StatePair", b => + { + b.Property("Key") + .HasColumnType("INTEGER"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Key"); + + b.ToTable("StatePairs"); + }); + + modelBuilder.Entity("Flagrum.Web.Persistence.Entities.AssetExplorerNode", b => + { + b.HasOne("Flagrum.Web.Persistence.Entities.AssetExplorerNode", "Parent") + .WithMany("Children") + .HasForeignKey("ParentId"); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("Flagrum.Web.Persistence.Entities.AssetUri", b => + { + b.HasOne("Flagrum.Web.Persistence.Entities.ArchiveLocation", "ArchiveLocation") + .WithMany("AssetUris") + .HasForeignKey("ArchiveLocationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ArchiveLocation"); + }); + + modelBuilder.Entity("Flagrum.Web.Persistence.Entities.ArchiveLocation", b => + { + b.Navigation("AssetUris"); + }); + + modelBuilder.Entity("Flagrum.Web.Persistence.Entities.AssetExplorerNode", b => + { + b.Navigation("Children"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Flagrum.Web/Services/AppStateService.cs b/Flagrum.Web/Services/AppStateService.cs index 96b558af..0caed440 100644 --- a/Flagrum.Web/Services/AppStateService.cs +++ b/Flagrum.Web/Services/AppStateService.cs @@ -2,6 +2,8 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using Flagrum.Web.Persistence; +using Flagrum.Web.Persistence.Entities; using Newtonsoft.Json; namespace Flagrum.Web.Services; @@ -15,10 +17,14 @@ public class AppStateService { private readonly AppStateData _data; private readonly SettingsService _settings; + private readonly FlagrumDbContext _context; - public AppStateService(SettingsService settings) + public AppStateService( + SettingsService settings, + FlagrumDbContext context) { _settings = settings; + _context = context; if (File.Exists(_settings.StatePath)) { @@ -38,6 +44,8 @@ public AppStateService(SettingsService settings) public IList UnmanagedEntries { get; set; } = new List(); public bool IsModListInitialized { get; set; } + public AssetExplorerNode Node { get; set; } + public int ActiveCategoryFilter { get; set; } = 0; public int ActiveModTypeFilter { get; set; } = -1; diff --git a/Flagrum.Web/Services/BinmodBuilder.cs b/Flagrum.Web/Services/BinmodBuilder.cs index 5b05cc70..92e2037c 100644 --- a/Flagrum.Web/Services/BinmodBuilder.cs +++ b/Flagrum.Web/Services/BinmodBuilder.cs @@ -13,24 +13,18 @@ namespace Flagrum.Web.Services; public class BinmodBuilder { - private readonly BinmodTypeHelper _binmodType; private readonly EntityPackageBuilder _entityPackageBuilder; private readonly Modmeta _modmeta; - private readonly SettingsService _settings; private Binmod _mod; private Packer _packer; public BinmodBuilder( - SettingsService settings, EntityPackageBuilder entityPackageBuilder, - Modmeta modmeta, - BinmodTypeHelper binmodType) + Modmeta modmeta) { - _settings = settings; _entityPackageBuilder = entityPackageBuilder; _modmeta = modmeta; - _binmodType = binmodType; } public void Initialise(Binmod mod, BuildContext context) @@ -201,7 +195,7 @@ public void AddFmd(int modelIndex, FmdData fmd) var model = OutfitTemplate.Build(_mod.ModDirectoryName, modelName, modelNamePrefix, fmd.Gpubin); var replacer = new ModelReplacer(model, fmd.Gpubin); - model = replacer.Replace(); + model = replacer.Replace(_mod.Type == (int)BinmodType.Character); if (_mod.Type is (int)BinmodType.Weapon or (int)BinmodType.Multi_Weapon) { diff --git a/Flagrum.Web/Services/DependencyInjection.cs b/Flagrum.Web/Services/DependencyInjection.cs index d6c36aee..80716dea 100644 --- a/Flagrum.Web/Services/DependencyInjection.cs +++ b/Flagrum.Web/Services/DependencyInjection.cs @@ -1,4 +1,5 @@ -using Microsoft.Extensions.DependencyInjection; +using Flagrum.Web.Persistence; +using Microsoft.Extensions.DependencyInjection; namespace Flagrum.Web.Services; @@ -6,7 +7,9 @@ public static class DependencyInjection { public static IServiceCollection AddFlagrum(this IServiceCollection services) { + services.AddDbContext(); services.AddSingleton(); + services.AddSingleton(); services.AddScoped(); services.AddSingleton(); services.AddScoped(); @@ -15,6 +18,16 @@ public static IServiceCollection AddFlagrum(this IServiceCollection services) services.AddScoped(); services.AddScoped(); services.AddScoped(); + + services.AddBlazorContextMenu(options => + { + // options.ConfigureTemplate(defaultTemplate => + // { + // defaultTemplate.MenuCssClass = "context-menu"; + // defaultTemplate.MenuItemCssClass = "context-menu-item"; + // }); + }); + return services; } } \ No newline at end of file diff --git a/Flagrum.Web/Services/TextureConverter.cs b/Flagrum.Web/Services/TextureConverter.cs index 2177dbcc..96de00e2 100644 --- a/Flagrum.Web/Services/TextureConverter.cs +++ b/Flagrum.Web/Services/TextureConverter.cs @@ -187,35 +187,26 @@ private byte[] ConvertDds(TextureType type, string name, byte[] data) private ScratchImage BuildDds(TextureType type, ScratchImage image) { - // Run necessary conversion methods on image + // var filterFlags = TEX_FILTER_FLAGS.CUBIC; + // var compressFlags = TEX_COMPRESS_FLAGS.DEFAULT; + // var metadata = image.GetMetadata(); + // if (metadata.Format is DXGI_FORMAT.B8G8R8A8_UNORM_SRGB + // or DXGI_FORMAT.B8G8R8X8_UNORM_SRGB + // or DXGI_FORMAT.R8G8B8A8_UNORM_SRGB) + // { + // compressFlags = TEX_COMPRESS_FLAGS.SRGB; + // filterFlags |= TEX_FILTER_FLAGS.SRGB; + // } + switch (type) { - case TextureType.Normal: - image = image.GenerateMipMaps(TEX_FILTER_FLAGS.LINEAR, 0); - image = image.Compress(DXGI_FORMAT.BC5_UNORM, TEX_COMPRESS_FLAGS.DEFAULT | TEX_COMPRESS_FLAGS.PARALLEL, - 0.5f); - break; - case TextureType.Greyscale: - image = image.GenerateMipMaps(TEX_FILTER_FLAGS.LINEAR, 0); + case TextureType.AmbientOcclusion: + image = image.GenerateMipMaps(TEX_FILTER_FLAGS.CUBIC, 0); image = image.Compress(DXGI_FORMAT.BC4_UNORM, TEX_COMPRESS_FLAGS.DEFAULT | TEX_COMPRESS_FLAGS.PARALLEL, 0.5f); break; - case TextureType.Preview or TextureType.Thumbnail: - var metadata = image.GetMetadata(); - if (type == TextureType.Preview && !(metadata.Width == 600 && metadata.Height == 600)) - { - image = image.Resize(600, 600, TEX_FILTER_FLAGS.DEFAULT); - } - else if (type == TextureType.Thumbnail && !(metadata.Width == 168 && metadata.Height == 242)) - { - image = image.Resize(168, 242, TEX_FILTER_FLAGS.DEFAULT); - } - - image = image.Compress(DXGI_FORMAT.BC1_UNORM, - TEX_COMPRESS_FLAGS.SRGB_OUT | TEX_COMPRESS_FLAGS.PARALLEL, 0.5f); - break; default: - image = image.GenerateMipMaps(TEX_FILTER_FLAGS.SRGB, 0); + image = image.GenerateMipMaps(TEX_FILTER_FLAGS.CUBIC, 0); image = image.Compress(DXGI_FORMAT.BC1_UNORM, TEX_COMPRESS_FLAGS.SRGB | TEX_COMPRESS_FLAGS.PARALLEL, 0.5f); break; diff --git a/Flagrum.Web/Services/UriMapper.cs b/Flagrum.Web/Services/UriMapper.cs new file mode 100644 index 00000000..559972ca --- /dev/null +++ b/Flagrum.Web/Services/UriMapper.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Flagrum.Core.Archive; +using Flagrum.Web.Persistence; +using Flagrum.Web.Persistence.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace Flagrum.Web.Services; + +public class UriMapper +{ + private readonly FlagrumDbContext _context; + private readonly SettingsService _settings; + private ConcurrentDictionary> _assets; + + public UriMapper( + FlagrumDbContext context, + SettingsService settings) + { + _context = context; + _settings = settings; + } + + public void RegenerateMap() + { + _context.Database.ExecuteSqlRaw($"DELETE FROM {nameof(_context.AssetExplorerNodes)};"); + _context.Database.ExecuteSqlRaw($"DELETE FROM {nameof(_context.ArchiveLocations)};"); + _context.Database.ExecuteSqlRaw($"DELETE FROM SQLITE_SEQUENCE WHERE name='{nameof(_context.AssetExplorerNodes)}';"); + _context.Database.ExecuteSqlRaw($"DELETE FROM SQLITE_SEQUENCE WHERE name='{nameof(_context.ArchiveLocations)}';"); + + _assets = new ConcurrentDictionary>(); + var assetUris = new List(); + + MapDirectory(_settings.GameDataDirectory); + Parallel.ForEach(Directory.EnumerateDirectories(_settings.GameDataDirectory), GenerateMapRecursively); + + var root = new AssetExplorerNode + { + Name = "data://" + }; + + foreach (var (archive, uris) in _assets) + { + foreach (var uri in uris) + { + if (uri.StartsWith("CRAF")) + { + continue; + } + + assetUris.Add(new AssetUri + { + ArchiveLocation = archive, + Uri = "data://" + uri + }); + + var tokens = uri.Split('/'); + var currentNode = root; + foreach (var token in tokens) + { + var subdirectory = currentNode.Children + .FirstOrDefault(c => c.Name == token); + + if (subdirectory == null) + { + subdirectory = new AssetExplorerNode + { + Name = token, + Parent = currentNode + }; + + currentNode.Children.Add(subdirectory); + } + + currentNode = subdirectory; + } + } + } + + var distinctUris = assetUris.DistinctBy(a => a.Uri); + _context.Add(root); + _context.AddRange(distinctUris); + _context.SaveChanges(); + } + + private void GenerateMapRecursively(string directory) + { + MapDirectory(directory); + Parallel.ForEach(Directory.EnumerateDirectories(directory), GenerateMapRecursively); + } + + private void MapDirectory(string directory) + { + foreach (var file in Directory.EnumerateFiles(directory, "*.earc")) + { + using var unpacker = new Unpacker(file); + var archive = new ArchiveLocation {Path = file.Replace(_settings.GameDataDirectory + "\\", "")}; + _assets.TryAdd(archive, unpacker.Files + .Where(f => !f.Flags.HasFlag(ArchiveFileFlag.Reference)) + .Select(f => f.Uri.Replace("data://", ""))); + } + } +} \ No newline at end of file diff --git a/Flagrum.Web/_Imports.razor b/Flagrum.Web/_Imports.razor index 47f91805..7a22ccd1 100644 --- a/Flagrum.Web/_Imports.razor +++ b/Flagrum.Web/_Imports.razor @@ -18,4 +18,6 @@ @using Blazor.Diagrams.Components.Renderers @using Blazor.Diagrams.Components.Groups @using Microsoft.Extensions.Logging -@using Blazor.Extensions.Canvas \ No newline at end of file +@using Blazor.Extensions.Canvas +@using BlazorContextMenu +@using Flagrum.Web.Persistence \ No newline at end of file diff --git a/Flagrum.sln b/Flagrum.sln index 1d0d135e..e84bd688 100644 --- a/Flagrum.sln +++ b/Flagrum.sln @@ -13,8 +13,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flagrum.Desktop", "Flagrum. EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flagrum.Web", "Flagrum.Web\Flagrum.Web.csproj", "{4C3303C2-3CA7-4021-8098-E882D8381B23}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ItemIdFixer", "ItemIdFixer\ItemIdFixer.csproj", "{1C317DE8-9522-4FCD-892C-5FA81F4F572C}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -85,18 +83,6 @@ Global {4C3303C2-3CA7-4021-8098-E882D8381B23}.Release|x64.Build.0 = Release|Any CPU {4C3303C2-3CA7-4021-8098-E882D8381B23}.Release|x86.ActiveCfg = Release|Any CPU {4C3303C2-3CA7-4021-8098-E882D8381B23}.Release|x86.Build.0 = Release|Any CPU - {1C317DE8-9522-4FCD-892C-5FA81F4F572C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1C317DE8-9522-4FCD-892C-5FA81F4F572C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1C317DE8-9522-4FCD-892C-5FA81F4F572C}.Debug|x64.ActiveCfg = Debug|Any CPU - {1C317DE8-9522-4FCD-892C-5FA81F4F572C}.Debug|x64.Build.0 = Debug|Any CPU - {1C317DE8-9522-4FCD-892C-5FA81F4F572C}.Debug|x86.ActiveCfg = Debug|Any CPU - {1C317DE8-9522-4FCD-892C-5FA81F4F572C}.Debug|x86.Build.0 = Debug|Any CPU - {1C317DE8-9522-4FCD-892C-5FA81F4F572C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1C317DE8-9522-4FCD-892C-5FA81F4F572C}.Release|Any CPU.Build.0 = Release|Any CPU - {1C317DE8-9522-4FCD-892C-5FA81F4F572C}.Release|x64.ActiveCfg = Release|Any CPU - {1C317DE8-9522-4FCD-892C-5FA81F4F572C}.Release|x64.Build.0 = Release|Any CPU - {1C317DE8-9522-4FCD-892C-5FA81F4F572C}.Release|x86.ActiveCfg = Release|Any CPU - {1C317DE8-9522-4FCD-892C-5FA81F4F572C}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE