ZenHAX

Free Game Research Forum | Official QuickBMS support | twitter @zenhax | SSL HTTPS://zenhax.com
It is currently Tue Apr 20, 2021 9:54 pm

All times are UTC




Post new topic  Reply to topic  [ 12 posts ] 
Author Message
PostPosted: Mon Mar 01, 2021 5:30 pm 

Joined: Mon Mar 01, 2021 5:27 pm
Posts: 1
It's possible to unpack the game's QDF archives using ZXstudio's QDF tool
http://zxstudio.org/blog/open-horizon-downloads/

/target/model_id/mech/plyr contains the meshes for hero aircraft
/target/model_id/mech/airp/o_* contains the meshes for NPC aircraft
/target/model_id/mech/airp/d_* contains the meshes for destroyed aircraft
/target/model_id/tdb/plyr and airp contains the textures for their respective meshes

I tried using the FHM tool for Mobile Suit Gundam Extreme Vs Full Boost which has the same publisher, but it's not compatible.

The .img textures have DDS DXT4 in their headers but renaming the extension doesn't prove to be a quick fix.

You can get the files for the Tornado Gr4 below if anyone wants to take a stab at it.
http://www.mediafire.com/file/ozovxovel ... d.zip/file


Top
   
PostPosted: Sat Mar 20, 2021 5:04 pm 

Joined: Wed Nov 02, 2016 4:15 am
Posts: 12
Hi, in fact you're right on the FHM storing the model data, most specific the pcom.fhm, but yeah, despite Gundam also begin developed by Bandai Namco, and using the same in-house container, the structure of both is very different, as NDXR meshes tends to get different variations also, making it different from other games and incompatible with plugins developed for some.

The .img textures of the aircraft textures, isn't something too ordinary(is obviously an exception with .nut texture files BTW), simply rename it to .dds, and drag it into photoshop or GIMP, if you need a viewer, download WTV from Nvidia, or XnView.

Now about the models, i managed to get some help to crack the structure of them, despite not begin ACAH exactly, i managed to reverse the F-16XL from Ace Combat Infinity, the main difference of both structures is the ACAH:EE and XBOX 360 ones are in Big Endian, when the PS3 ones are in Little Endian, along there are some minimum bytes the Big Endian pcom.fhm got that i wasn't able to debug correctly by memory dump. unfortunately, the process turns to be tedious by manual input, as the ACAH and ACI models, mostly the default aircraft, tends to get almost 70 to 100 meshes, along it obviously lack the armatures, but it wouldn't be such a issue if you got brief knowledge at replicating them for some parts, compared to a human rig.
Image

I would be interresed at least if someone could be developing a Blender plugin preferably, to automize the process for importing all parts of the mesh, mostly i would prefer using this method compared to the previous one i managed to develop using Renderdoc, the main negative point of it is the lack of UV maps intact, along the model would be squizzed in vertical axis too. for the people interresed, i would recommend the hex2obj explained tutorial:
https://forum.xentax.com/viewtopic.php?f=29&t=17890
along using the triangle strip reference from this thread:
https://forum.xentax.com/viewtopic.php?t=22230#p163865


Top
   
PostPosted: Mon Mar 22, 2021 4:27 pm 
User avatar

Joined: Fri Aug 08, 2014 12:59 am
Posts: 8
Image

Quote:
Below is a python script I wrote for blender that was tested in the latest version (currently 2.92.0)

usage: start blender, goto the script tab then press the 'NEW' button, then paste the below script into the editor. To execute the script press ALT+P or press the [>] play button. When Pressed a file selection box will appear which will prompt to open a file to import. Once the script has executed once, thereafter the file prompt may then be accessed through the File > Import Dialog until blender is restarted


Code:
""" ======================================================================

    PythonScript:   [PC] Ace Combat Assault Horizon
    Author:         mariokart64n
    Date:           March 22, 2021
    Version:        0.1

    ======================================================================

    ChangeLog:

    2021-03-22
        Script Wrote

    2021-03-23
        Adapted to work with PS3 Memory dump from Ace Combat Infinity

    ====================================================================== """

import bpy  # Needed to interface with blender
import struct  # Needed for Binary Reader
import math
from pathlib import Path  # Needed for os stuff


useOpenDialog = True


signed, unsigned = 0, 1  # Enums for read function
seek_set, seek_cur, seek_end = 0, 1, 2  # Enums for seek function
SEEK_ABS, SEEK_REL, SEEK_END = 0, 1, 2  # Enums for seek function


def messageBox(message="", title="Message Box", icon='INFO'):
    def draw(self, context): self.layout.label(text=message)

    bpy.context.window_manager.popup_menu(draw, title=title, icon=icon)
    return None


def clearListener(len=64):
    for i in range(0, len): print('')


def getFilenameFile(file):  # returns: "myImage"
    return Path(file).stem


def getFilenameType(file):  # returns: ".jpg"
    return Path(file).suffix


def toUpper(string):
    return string.upper()


def findItem(array, value):
    index = -1
    try:
        index = array.index(value)
    except:
        pass
    return index


def append(array, value):
    array.append(value)
    return None


def appendIfUnique(array, value):
    try:
        array.index(value)
    except:
        array.append(value)
    return None


class StandardMaterial:
    data = None
    bsdf = None

    def __init__(self, name="Material"):
        # make material
        self.data = bpy.data.materials.new(name=name)
        self.data.use_nodes = True
        self.data.use_backface_culling = True
        self.bsdf = self.data.node_tree.nodes["Principled BSDF"]
        self.bsdf.label = "Standard"
        return None

    def diffuse(self, colour=(0.0, 0.0, 0.0, 0.0), name="Diffuse"):
        rgbaColor = self.data.node_tree.nodes.new('ShaderNodeRGB')
        rgbaColor.label = name
        rgbaColor.outputs[0].default_value = (colour[0], colour[1], colour[2], colour[3])
        if self.bsdf != None:
            self.data.node_tree.links.new(self.bsdf.inputs['Base Color'], rgbaColor.outputs['Color'])
        return None

    def diffuseMap(self, imageTex=None):
        if imageTex != None and self.bsdf != None:
            self.data.node_tree.links.new(self.bsdf.inputs['Base Color'], imageTex.outputs['Color'])
        return None

    def opacityMap(self, imageTex=None):
        if imageTex != None and self.bsdf != None:
            self.data.blend_method = 'BLEND'
            self.data.shadow_method = 'HASHED'
            self.data.show_transparent_back = False
            self.data.node_tree.links.new(self.bsdf.inputs['Alpha'], imageTex.outputs['Alpha'])
        return None

    def normalMap(self, imageNode=None):
        if imageTex != None and self.bsdf != None:
            imageTex.image.colorspace_settings.name = 'Linear'
            normMap = self.data.node_tree.nodes.new('ShaderNodeNormalMap')
            normMap.label = 'ShaderNodeNormalMap'
            self.data.node_tree.links.new(normMap.inputs['Color'], imageTex.outputs['Color'])
            self.data.node_tree.links.new(self.bsdf.inputs['Normal'], normMap.outputs['Normal'])
        return None

    def specularMap(self, imageNode=None, invert=True):
        if imageTex != None and self.bsdf != None:
            if invert:
                invertRGB = self.data.node_tree.nodes.new('ShaderNodeInvert')
                invertRGB.label = 'ShaderNodeInvert'
                self.data.node_tree.links.new(invertRGB.inputs['Color'], imageTex.outputs['Color'])
                self.data.node_tree.links.new(self.bsdf.inputs['Roughness'], invertRGB.outputs['Color'])
            else:
                self.data.node_tree.links.new(self.bsdf.inputs['Roughness'], imageTex.outputs['Color'])
        return None


class fopen:
    little_endian = True
    file = ""
    mode = 'rb'
    data = bytearray()
    size = 0
    pos = 0
    isGood = False

    def __init__(self, filename=None, mode='rb', isLittleEndian=True):
        if mode == 'rb':
            if filename != None and Path(filename).is_file():
                self.data = open(filename, mode).read()
                self.size = len(self.data)
                self.pos = 0
                self.mode = mode
                self.file = filename
                self.little_endian = isLittleEndian
                self.isGood = True
        else:
            self.file = filename
            self.mode = mode
            self.data = bytearray()
            self.pos = 0
            self.size = 0
            self.little_endian = isLittleEndian
            self.isGood = False

        return None

    # def __del__(self):
    #    self.flush()

    def resize(self, dataSize=0):
        if dataSize > 0:
            self.data = bytearray(dataSize)
        else:
            self.data = bytearray()
        self.pos = 0
        self.size = dataSize
        self.isGood = False
        return None

    def flush(self):
        print("flush")
        print("file:\t%s" % self.file)
        print("isGood:\t%s" % self.isGood)
        print("size:\t%s" % len(self.data))
        if self.file != "" and not self.isGood and len(self.data) > 0:
            self.isGood = True

            s = open(self.file, 'w+b')
            s.write(self.data)
            s.close()

    def read_and_unpack(self, unpack, size):
        '''
          Charactor, Byte-order
          @,         native, native
          =,         native, standard
          <,         little endian
          >,         big endian
          !,         network

          Format, C-type,         Python-type, Size[byte]
          c,      char,           byte,        1
          b,      signed char,    integer,     1
          B,      unsigned char,  integer,     1
          h,      short,          integer,     2
          H,      unsigned short, integer,     2
          i,      int,            integer,     4
          I,      unsigned int,   integer,     4
          f,      float,          float,       4
          d,      double,         float,       8
        '''
        value = 0
        if self.size > 0 and self.pos + size < self.size:
            value = struct.unpack_from(unpack, self.data, self.pos)[0]
            self.pos += size
        return value

    def pack_and_write(self, pack, size, value):
        if self.pos + size > self.size:
            self.data.extend(b'\x00' * ((self.pos + size) - self.size))
            self.size = self.pos + size
        try:
            struct.pack_into(pack, self.data, self.pos, value)
        except:
            print('Pos:\t%i / %i (buf:%i) [val:%i:%i:%s]' % (self.pos, self.size, len(self.data), value, size, pack))
            pass
        self.pos += size
        return None

    def set_pointer(self, offset):
        self.pos = offset
        return None

    def set_endian(self, isLittle = True):
        self.little_endian = isLittle
        return isLittle

def fclose(bitStream):
    bitStream.flush()
    bitStream.isGood = False


def fseek(bitStream, offset, dir):
    if dir == 0:
        bitStream.set_pointer(offset)
    elif dir == 1:
        bitStream.set_pointer(bitStream.pos + offset)
    elif dir == 2:
        bitStream.set_pointer(bitStream.pos - offset)
    return None


def ftell(bitStream):
    return bitStream.pos


def readByte(bitStream, isSigned=0):
    fmt = 'b' if isSigned == 0 else 'B'
    return (bitStream.read_and_unpack(fmt, 1))


def readShort(bitStream, isSigned=0):
    fmt = '>' if not bitStream.little_endian else '<'
    fmt += 'h' if isSigned == 0 else 'H'
    return (bitStream.read_and_unpack(fmt, 2))


def readLong(bitStream, isSigned=0):
    fmt = '>' if not bitStream.little_endian else '<'
    fmt += 'i' if isSigned == 0 else 'I'
    return (bitStream.read_and_unpack(fmt, 4))

def readFloat(bitStream):
    fmt = '>f' if not bitStream.little_endian else '<f'
    return (bitStream.read_and_unpack(fmt, 4))


def readHalf(bitStream):
    uint16 = bitStream.read_and_unpack('>H' if not bitStream.little_endian else '<H', 2)
    uint32 = (
            (((uint16 & 0x03FF) << 0x0D) | ((((uint16 & 0x7C00) >> 0x0A) + 0x70) << 0x17)) |
            (((uint16 >> 0x0F) & 0x00000001) << 0x1F)
    )
    return struct.unpack('f', struct.pack('I', uint32))[0]


def readString(bitStream, length=0):
    string = ''
    pos = bitStream.pos
    lim = length if length != 0 else bitStream.size - bitStream.pos
    for i in range(0, lim):
        b = bitStream.read_and_unpack('B', 1)
        if b != 0:
            string += chr(b)
        else:
            if length > 0:
                bitStream.set_pointer(pos + length)
            break
    return string


def mesh_validate (vertices=[], faces=[]):
    #
    # Returns True if mesh is BAD
    #
    # check face index bound
    face_min = 0
    face_max = len(vertices) - 1
   
    for face in faces:
        for side in face:
            if side < face_min or side > face_max:
                print("Face Index Out of Range:\t[%i / %i]" % (side, face_max))
                return True
    return False

def mesh(
    vertices=[],
    faces=[],
    materialIDs=[],
    tverts=[],
    normals=[],
    colours=[],
    materials=[],
    mscale=1.0,
    flipAxis=False,
    obj_name="Object",
    lay_name='',
    position = (0.0, 0.0, 0.0)
    ):
    #
    # This function is pretty, ugly
    # imports the mesh into blender
    #
    # Clear Any Object Selections
    # for o in bpy.context.selected_objects: o.select = False
    bpy.context.view_layer.objects.active = None
   
    # Get Collection (Layers)
    if lay_name != '':
        # make collection
        layer = bpy.data.collections.get(lay_name)
        if layer == None:
            layer = bpy.data.collections.new(lay_name)
            bpy.context.scene.collection.children.link(layer)
    else:
        if len(bpy.data.collections) == 0:
            layer = bpy.data.collections.new("Collection")
            bpy.context.scene.collection.children.link(layer)
        else:
            try:
                layer = bpy.data.collections[bpy.context.view_layer.active_layer_collection.name]
            except:
                layer = bpy.data.collections[0]
   

    # make mesh
    msh = bpy.data.meshes.new('Mesh')

    # msh.name = msh.name.replace(".", "_")

    # Apply vertex scaling
    # mscale *= bpy.context.scene.unit_settings.scale_length
    if len(vertices) > 0:
        vertArray = [[float] * 3] * len(vertices)
        if flipAxis:
            for v in range(0, len(vertices)):
                vertArray[v] = (
                    vertices[v][0] * mscale,
                    -vertices[v][2] * mscale,
                    vertices[v][1] * mscale
                )
        else:
            for v in range(0, len(vertices)):
                vertArray[v] = (
                    vertices[v][0] * mscale,
                    vertices[v][1] * mscale,
                    vertices[v][2] * mscale
                )

    # assign data from arrays
    if mesh_validate(vertArray, faces):
        # Erase Mesh
        msh.user_clear()
        bpy.data.meshes.remove(msh)
        print("Mesh Deleted!")
        return None
   
    msh.from_pydata(vertArray, [], faces)

    # set surface to smooth
    msh.polygons.foreach_set("use_smooth", [True] * len(msh.polygons))

    # Set Normals
    if len(faces) > 0:
        if len(normals) > 0:
            msh.use_auto_smooth = True
            if len(normals) == (len(faces) * 3):
                msh.normals_split_custom_set(normals)
            else:
                normArray = [[float] * 3] * (len(faces) * 3)
                if flipAxis:
                    for i in range(0, len(faces)):
                        for v in range(0, 3):
                            normArray[(i * 3) + v] = (
                                [normals[faces[i][v]][0],
                                 -normals[faces[i][v]][2],
                                 normals[faces[i][v]][1]]
                            )
                else:
                    for i in range(0, len(faces)):
                        for v in range(0, 3):
                            normArray[(i * 3) + v] = (
                                [normals[faces[i][v]][0],
                                 normals[faces[i][v]][1],
                                 normals[faces[i][v]][2]]
                            )
                msh.normals_split_custom_set(normArray)

        # create texture corrdinates
        #print("tverts ", len(tverts))
        # this is just a hack, i just add all the UVs into the same space <<<
        if len(tverts) > 0:
            uvw = msh.uv_layers.new()
            # if len(tverts) == (len(faces) * 3):
            #    for v in range(0, len(faces) * 3):
            #        msh.uv_layers[uvw.name].data[v].uv = tverts[v]
            # else:
            uvwArray = [[float] * 2] * len(tverts[0])
            for i in range(0, len(tverts[0])):
                uvwArray[i] = [0.0, 0.0]

            for v in range(0, len(tverts[0])):
                for i in range(0, len(tverts)):
                    uvwArray[v][0] += tverts[i][v][0]
                    uvwArray[v][1] += 1.0 - tverts[i][v][1]

            for i in range(0, len(faces)):
                for v in range(0, 3):
                    msh.uv_layers[uvw.name].data[(i * 3) + v].uv = (
                        uvwArray[faces[i][v]][0],
                        uvwArray[faces[i][v]][1]
                    )


    # Create Face Maps?
    # msh.face_maps.new()

    # Update Mesh
    msh.update()

    # Check mesh is Valid
    # Without this blender may crash!!! lulz
    # However the check will throw false positives so
    # and additional or a replacement valatiation function
    # would be required
   
    if msh.validate():
        print("Mesh Failed Validation")

       

    # Assign Mesh to Object
    obj = bpy.data.objects.new(obj_name, msh)
    obj.location = position
    # obj.name = obj.name.replace(".", "_")

    for i in range(0, len(materials)):

        if len(obj.material_slots) < (i + 1):
            # if there is no slot then we append to create the slot and assign
            obj.data.materials.append(materials[i])
        else:
            # we always want the material in slot[0]
            obj.material_slots[0].material = materials[i]
        # obj.active_material = obj.material_slots[i].material

    if len(materialIDs) == len(obj.data.polygons):
        for i in range(0, len(materialIDs)):
            obj.data.polygons[i].material_index = materialIDs[i] % len(materials)
    elif len(materialIDs) > 0:
        print("Error:\tMaterial Index Out of Range")

    # obj.data.materials.append(material)
    layer.objects.link(obj)

    # Generate a Material
    # img_name = "Test.jpg"  # dummy texture
    # mat_count = len(texmaps)

    # if mat_count == 0 and len(materialIDs) > 0:
    #    for i in range(0, len(materialIDs)):
    #        if (materialIDs[i] + 1) > mat_count: mat_count = materialIDs[i] + 1

    # Assign Material ID's
    bpy.context.view_layer.objects.active = obj
    bpy.ops.object.mode_set(mode='EDIT', toggle=False)
    bpy.context.tool_settings.mesh_select_mode = [False, False, True]

    bpy.ops.object.mode_set(mode='OBJECT')
    # materialIDs

    # Redraw Entire Scene
    # bpy.context.scene.update()

    return obj


def deleteScene(include=[]):
    if len(include) > 0:
        # Exit and Interactions
        if bpy.context.view_layer.objects.active != None:
            bpy.ops.object.mode_set(mode='OBJECT')

        # Select All
        bpy.ops.object.select_all(action='SELECT')

        # Loop Through Each Selection
        for o in bpy.context.view_layer.objects.selected:
            for t in include:
                if o.type == t:
                    bpy.data.objects.remove(o, do_unlink=True)
                    break

        # De-Select All
        bpy.ops.object.select_all(action='DESELECT')
    return None

class ndxr_entry_info_cmd_table2:
    unk081 = 0
    name_addr = 0
    name = ""
    unk083 = 0
    unk084 = 0
    unk085 = 0.0
    unk086 = 0.0
    unk087 = 0.0
    unk088 = 0.0

    def read_info_cmd_table2(self, strings_addr=0, f=fopen()):
        self.unk081 = readLong(f, unsigned)
        self.name_addr = readLong(f, unsigned)
        self.unk083 = readLong(f, unsigned)
        self.unk084 = readLong(f, unsigned)
        self.unk085 = readFloat(f)
        self.unk086 = readFloat(f)
        self.unk087 = readFloat(f)
        self.unk088 = readFloat(f)
        pos = ftell(f)
        fseek(f, (strings_addr + self.name_addr), seek_set)
        self.name = readString(f)
        fseek(f, pos, seek_set)


class ndxr_entry_info_cmd_table1:  # 24 bytes
    unk061 = 0
    unk062 = 0
    unk063 = 0
    unk064 = 0
    unk065 = 0
    unk066 = 0
    unk067 = 0
    unk068 = 0
    unk069 = 0
    unk070 = 0
    unk071 = 0
    unk072 = 0
    unk073 = 0
    unk074 = 0
    unk075 = 0
    unk076 = 0

    def read_info_cmd_table1(self, f=fopen()):
        self.unk061 = readByte(f, unsigned)
        self.unk062 = readShort(f, unsigned)
        self.unk063 = readLong(f, unsigned)
        self.unk064 = readByte(f, unsigned)
        self.unk065 = readShort(f, unsigned)
        self.unk066 = readLong(f, unsigned)
        self.unk067 = readByte(f, unsigned)
        self.unk068 = readByte(f, unsigned)
        self.unk069 = readByte(f, unsigned)
        self.unk070 = readByte(f, unsigned)
        self.unk071 = readByte(f, unsigned)
        self.unk072 = readByte(f, unsigned)
        self.unk073 = readByte(f, unsigned)
        self.unk074 = readByte(f, unsigned)
        self.unk075 = readByte(f, unsigned)
        self.unk076 = readByte(f, unsigned)


class ndxr_entry_info_cmd:
    unk041 = 0
    unk042 = 0
    unk043 = 0
    unk044 = 0
    unk045 = 0
    table1_count = 0  # count
    unk046 = 0
    unk047 = 0
    unk048 = 0
    unk049 = 0
    unk050 = 0
    table1 = []
    unk051 = 0
    unk052 = 0
    unk053 = 0
    table2 = []

    def read_info_cmd(self, strings_addr=0, f=fopen()):
        self.unk041 = readByte(f, unsigned)
        self.unk042 = readShort(f, unsigned)
        self.unk043 = readByte(f, unsigned)
        self.unk044 = readShort(f, unsigned)
        self.unk045 = readLong(f, unsigned)
        self.table1_count = readShort(f, unsigned)
        self.unk046 = readShort(f, unsigned)
        self.unk047 = readLong(f, unsigned)
        self.unk048 = readLong(f, unsigned)
        self.unk049 = readByte(f, unsigned)
        self.unk050 = readShort(f, unsigned)
        if self.table1_count > 0:
            self.table1 = [ndxr_entry_info_cmd_table1] * self.table1_count
            for i in range(0, self.table1_count):
                self.table1[i] = ndxr_entry_info_cmd_table1()
                self.table1[i].read_info_cmd_table1(f)
                self.unk051 = readByte(f, unsigned)
                self.unk052 = readShort(f, unsigned)
                self.unk053 = readLong(f, unsigned)
                i = -1
                while True:
                    i += 1
                    append(self.table2, (ndxr_entry_info_cmd_table2()))
                    self.table2[i].read_info_cmd_table2(strings_addr, f)
                    if self.table2[i].unk081 != 0x20: break


class ndxr_entry_info:
    face_addr = 0  # face buffer addr?
    vert_addr = 0  # vert buffer addr?
    unk033 = 0  # 0
    vert_count = 0  # vertex count?
    unk036 = 0  # 6
    unk037 = 0  # ? vertex format? 17=28bytes, 16=20bytes
    cmd_addr = 0
    cmd = ndxr_entry_info_cmd()
    unk038 = 0  # 0
    unk039 = 0  # 0
    unk040 = 0  # 0
    face_count = 0  # face count?
    unk042 = 0  # 0
    unk043 = 0  # 0
    unk044 = 0  # 0
    unk045 = 0  # 0

    def read_info(self, pos=0, strings_addr=0, f=fopen(), addr_off = 0):
        self.face_addr = readLong(f, unsigned)
        self.vert_addr = readLong(f, unsigned)
        self.unk033 = readLong(f, unsigned)
        self.vert_count = readShort(f, unsigned)
        self.unk036 = readByte(f, unsigned)
        self.unk037 = readByte(f, unsigned)
        self.cmd_addr = readLong(f, unsigned) - addr_off
        self.unk038 = readLong(f, unsigned)
        self.unk039 = readLong(f, unsigned)
        self.unk040 = readLong(f, unsigned)
        self.face_count = readShort(f, unsigned)
        self.unk042 = readShort(f, unsigned)
        self.unk043 = readLong(f, unsigned)
        self.unk044 = readLong(f, unsigned)
        self.unk045 = readLong(f, unsigned)
        if self.cmd_addr > 0:
            fseek(f, pos + self.cmd_addr, seek_set)
            self.cmd.read_info_cmd(strings_addr, f)


class ndxr_entry:
    unk011 = 0.0
    unk012 = 0.0
    unk013 = 0.0
    unk014 = 0.0
    unk015 = 0.0
    unk016 = 0.0
    unk017 = 0.0
    unk018 = 0
    name_addr = 0
    name = ""
    unk019 = 0
    unk020 = 0
    unk021 = 0
    unk022 = 0  # info count
    info_addr = 0
    info = []

    def read_entry(self, pos=0, strings_addr=0, f=fopen(), addr_off = 0):
        self.unk011 = readFloat(f)
        self.unk012 = readFloat(f)
        self.unk013 = readFloat(f)
        self.unk014 = readFloat(f)
        self.unk015 = readFloat(f)
        self.unk016 = readFloat(f)
        self.unk017 = readFloat(f)
        self.unk018 = readLong(f, unsigned)
        self.name_addr = readLong(f, unsigned)
        self.unk019 = readShort(f, unsigned)
        self.unk020 = readShort(f, unsigned)
        self.unk021 = readShort(f, unsigned)
        self.unk022 = readShort(f, unsigned)
        self.info_addr = readLong(f, unsigned) - addr_off
        fseek(f, (strings_addr + self.name_addr), seek_set)
        self.name = readString(f)
        if self.unk022 > 0 and self.info_addr > 0:
            self.info = [ndxr_entry_info] * self.unk022
            for i in range(0, self.unk022):
                fseek(f, (pos + self.info_addr + (i * 48)), seek_set)
                self.info[i] = ndxr_entry_info()
                self.info[i].read_info(pos, strings_addr, f, addr_off)


class ndxr_file:
    fileid = 0
    unk001 = 0
    unk002 = 0
    count = 0
    unk007 = 0
    unk003 = 0
    face_addr = 0
    face_size = 0
    vert_size = 0
    unk004 = 0
    unk005 = 0
    unk006 = [0.0, 0.0, 0.0]
    entries = []

    def read(self, f=fopen(), mscale = 1.0, col_name = "", addr_off = 0):
        pos = ftell(f)
        header_size = 48
        self.fileid = readLong(f, unsigned)
        self.unk001 = readLong(f, unsigned)
        self.unk002 = readShort(f, unsigned)
        self.count = readShort(f, unsigned)
        self.unk007 = readShort(f, unsigned)
        self.unk003 = readShort(f, unsigned)
        self.face_addr = readLong(f, unsigned)
        self.face_size = readLong(f, unsigned)
        self.vert_size = readLong(f, unsigned)
        self.unk004 = readLong(f, unsigned)
        self.unk005 = readLong(f, unsigned)
        self.unk006 = [readFloat(f), readFloat(f), readFloat(f)]

        self.face_addr += pos + header_size
        vert_addr = self.face_addr + self.face_size
        strings_addr = vert_addr + self.vert_size
        entry_size = 48
        vertArray = []
        normArray = []
        faceArray = []
        matidArray = []
        tvertArray = []
        msh = None
        tmp = []
        vertex_stride = 0
        face = [0, 0, 0]
        facePosition = 0
        faceCW = True
        maxIndex = 0

        if self.count > 0:
            self.entries = [ndxr_entry] * self.count
            for i in range(0, self.count):
                fseek(f, (pos + header_size + (i * entry_size)), seek_set)
                self.entries[i] = ndxr_entry()
                self.entries[i].read_entry(pos, strings_addr, f, addr_off)
       
       
           
       
        # Generate Size List to Estimate the Vertex Stride
        for i in range(0, self.count):
            for ii in range(0, self.entries[i].unk022):
                appendIfUnique(tmp, (self.entries[i].info[ii].vert_addr + vert_addr))

        append(tmp, strings_addr)
        tmp.sort()
       
        for i in range(0, self.count):  # mesh entry
            vertArray = []
            tvertArray = []
            faceArray = []
            normArray = []
            matidArray = []
            facePosition = 0
            for ii in range(0, self.entries[i].unk022):  # level of detail meshes?

                # Read Faces
                fseek(f, (self.face_addr + self.entries[i].info[ii].face_addr), seek_set)
                v = 0
               
                while v < self.entries[i].info[ii].face_count:
                    faceCW = True
                    face[0] = readShort(f, unsigned)
                    face[1] = readShort(f, unsigned)
                    v += 2
                    while v < self.entries[i].info[ii].face_count:
                        face[2] = readShort(f, unsigned)
                        v += 1
                        if face[0] == 0xFFFF or face[1] == 0xFFFF or face[2] == 0xFFFF: break
                        if face[0] != face[1] and face[1] != face[2] and face[0] != face[2]:
                            if faceCW:
                                append(faceArray, [face[0] + facePosition, face[1] + facePosition, face[2] + facePosition])
                            else:
                                append(faceArray, [face[0] + facePosition, face[2] + facePosition, face[1] + facePosition])
                            append(matidArray, ii)
                        faceCW = not faceCW
                        face = [face[1], face[2], face[0]]
                       
                facePosition += self.entries[i].info[ii].vert_count
                vertex_stride = 20
                # Reading Vertices
                if self.entries[i].info[ii].vert_count != 0:
                    vertex_stride = int(
                        (tmp[(findItem(tmp, self.entries[i].info[ii].vert_addr + vert_addr)) + 1] -
                        (self.entries[i].info[ii].vert_addr + vert_addr)) / self.entries[i].info[ii].vert_count
                        )
                    vertex_stride = int(vertex_stride - (vertex_stride % 4))

                # format "Vertex Addr:\t%\n" (entries[i].info[ii].vert_addr + vert_addr)
                # format "Vertex Count:\t%\n" entries[i].info[ii].vert_count

                # format "Vertex Format:\t%\n" entries[i].info[ii].unk037
                if self.entries[i].info[ii].unk036 == 0:
                    vertex_stride = 20
                elif self.entries[i].info[ii].unk036 == 6:
                    vertex_stride = 28
                elif self.entries[i].info[ii].unk036 == 7:
                    vertex_stride = 44
                else:
                    print("Error:\tUnsupported Vertex Stride [%i]" % self.entries[i].info[ii].unk036)

                # format "Vertex Stride:\t%\n" vertex_stride
                # vertArray[entries[i].info[ii].vert_count] = [0.0, 0.0, 0.0]
                for v in range(0, self.entries[i].info[ii].vert_count):
                    fseek(f, (vert_addr + self.entries[i].info[ii].vert_addr + (v * vertex_stride)),
                          seek_set)
                    append(vertArray, [readFloat(f), readFloat(f), readFloat(f)])
                    if vertex_stride >= 28:
                        fseek(f, 8, seek_cur)  # append normArray ([readHalf f, readHalf f, readHalf f] * (readHalf f))
                        append(tvertArray, [readFloat(f), readFloat(f), 0.0])
                    else:
                        fseek(f, 4, seek_cur)  # readLong(f) # normal
                        append(tvertArray, [readHalf(f), readHalf(f), 0.0])
           
            mats = []
            mat = None
            for ii in range(0, self.entries[i].unk022):
                mat = StandardMaterial()
                mats.append(mat.data)
           
            msh = mesh(
                vertices=vertArray,
                faces=faceArray,
                tverts=[tvertArray],
                materialIDs=matidArray,
                materials=mats,
                obj_name=self.entries[i].name,
                flipAxis=True,
                lay_name=col_name,
                mscale=mscale
                )
        return None

class fhm_table_addr_entry:
    unk021 = 0
    addr = 0
    def read_addr_entry(self, f=fopen()):
        self.unk021 = readLong(f, unsigned)
        self.addr = readLong(f, unsigned)


class fhm_table_entry:
    unk031=0
    unk032 = 0
    unk033 = 0
    addr = 0
    size = 0
    def read_entry(self, f=fopen()):
        self.unk031 = readShort(f)
        self.unk032 = readShort(f)
        self.unk033 = readLong(f, unsigned)
        self.addr = readLong(f, unsigned)
        self.size = readLong(f, unsigned)


class fhm_file:
    fileid=0
    unk001 = 0
    unk002 = 0
    unk003 = 0
    unk004 = 0
    unk005 = 0
    unk006 = 0
    unk007 = 0
    unk008 = 0
    unk009 = 0
    unk010 = 0
    unk011 = 0
    file_count = 0
    file_addr_table =[]
    file_table =[]
    def read(self, f=fopen()):
        self.fileid = readLong(f, unsigned)
        if self.fileid != 0x004D4846:
            print("Error:\tInvalid File Type\n")
            return False
       
        self.unk001 = readLong(f, unsigned)
        self.unk002 = readLong(f, unsigned)
        self.unk003 = readLong(f, unsigned)
        self.unk004 = readLong(f, unsigned)
        self.unk005 = readLong(f, unsigned)
        self.unk006 = readLong(f, unsigned)
        self.unk007 = readLong(f, unsigned)
        self.unk008 = readLong(f, unsigned)
        self.unk009 = readLong(f, unsigned)
        self.unk010 = readLong(f, unsigned)
        self.unk011 = readLong(f, unsigned)
        self.file_count = readLong(f, unsigned)
        if self.file_count > 0:
            self.file_addr_table = [fhm_table_addr_entry] * self.file_count
            self.file_table = [fhm_table_entry] * self.file_count
            for i in range(0, self.file_count):
                self.file_addr_table[i] = fhm_table_addr_entry()
                self.file_addr_table[i].read_addr_entry(f)
   
            for i in range(0, self.file_count):
                fseek(f, (0x30 + self.file_addr_table[i].addr), seek_set)
                self.file_table[i] = fhm_table_entry()
                self.file_table[i].read_entry(f)
        return True

def dump_fhm(file=""):
    f = fopen(file, "rb")
    s = None

    fseek(f, 0x30, seek_set)
    count = readLong(f)

    fseek(f, (count * 8), seek_cur)

    pos = ftell(f)
    addr = 0
    size = 0
    type = ""
    for i in range(0, count):
        fseek(f, (pos + (i * 16)), seek_set)

    readLong(f)
    readLong(f)
    addr = readLong(f)
    size = readLong(f)

    fseek(f, (addr + 0x30), seek_set)
    type = "."
    for x in range(0, 4):
        type += chr(readByte(f))

    s = fopen((file + "_" + str(i) + type), "wb")

    fseek(f, (addr + 0x30), seek_set)
    for x in range(0, size):
        writeByte(s, (readByte(f)))

    fclose(s)
    fclose(f)

def read(file="", mscale = 1.0):
    fhm = fhm_file()
    type = 0
    ndxr = ndxr_file()
    fileID = 0
   
    f = fopen(file, "rb")
    if f.isGood:
        fname = getFilenameFile(file)
        fileID = readLong(f, unsigned)
        fseek(f, 0, seek_set)

        if fileID == 0x004D4846:  # FHM
            fhm.read(f)

            for i in range(0, len(fhm.file_table)):
                fseek(f, (fhm.file_table[i].addr + 0x30), seek_set)
                type = readLong(f, unsigned)
                fseek(f, (fhm.file_table[i].addr + 0x30), seek_set)
                if type == 0x5258444E:
                    ndxr = ndxr_file()
                    ndxr.read(f, mscale, fname + "_" + str(i + 1))
                else:
                    print("#%i\tUnknown Block:\t%i\t@ 0x%s\n" % (i, type, hex(fhm.file_table[i].addr + 0x30)))


        elif fileID == 0x5258444E:  # NDXR
            ndxr = ndxr_file()
            ndxr.read(f, mscale)
           
        elif fileID == 0x3350444E:  # NDP3 (Memory Dump)
           
            # a sample was provided from a memory dump
            # in which the addresses are assigned to memory
            # and are out of bounds of the supplied file sample
            # For this we need to try and derive the address
            # relative to the file and not the PS3 Memory block
           
            f.set_endian(False)
            fseek(f, 0x0A, seek_set)
            count = readShort(f)
            fseek(f, 0x5C, seek_set)
            addr_off = readLong(f) - (48 + (count * 0x30))
            print("addr_off:\t%i" % addr_off)
            fseek(f, 0, seek_set)
           
           
            ndxr = ndxr_file()
            ndxr.read(f, mscale, "", addr_off)
        else:
            print("Error:\tUnsupported File Type\n")

        fclose(f)

    else:
        print("Error:\tFailed to Read File\n")


# Callback when file(s) are selected
def acecombat_ah_imp_callback(fpath="", files=[], clearScene=True, mscale = 1.0):
    if len(files) > 0 and clearScene: deleteScene(['MESH', 'ARMATURE'])
    for file in files:
        read(fpath + file.name, mscale)
    if len(files) > 0:
        messageBox("Done!")
        return True
    else:
        return False


# Wrapper that Invokes FileSelector to open files from blender
def acecombat_ah_imp(reload=False):
    # Un-Register Operator
    if reload and hasattr(bpy.types, "IMPORTHELPER_OT_acecombat_ah_imp"):  # print(bpy.ops.importhelper.acecombat_ah_imp.idname())

        try:
            bpy.types.TOPBAR_MT_file_import.remove(
                bpy.types.Operator.bl_rna_get_subclass_py('IMPORTHELPER_OT_acecombat_ah_imp').menu_func_import)
        except:
            print("Failed to Unregister2")

        try:
            bpy.utils.unregister_class(bpy.types.Operator.bl_rna_get_subclass_py('IMPORTHELPER_OT_acecombat_ah_imp'))
        except:
            print("Failed to Unregister1")

    # Define Operator
    class ImportHelper_acecombat_ah_imp(bpy.types.Operator):

        # Operator Path
        bl_idname = "importhelper.acecombat_ah_imp"
        bl_label = "Select File"

        # Operator Properties
        # filter_glob: bpy.props.StringProperty(default='*.jpg;*.jpeg;*.png;*.tif;*.tiff;*.bmp', options={'HIDDEN'})
        filter_glob: bpy.props.StringProperty(default='*.fhm;*.ndxr;*.ndp3', options={'HIDDEN'}, subtype='FILE_PATH')

        # Variables
        filepath: bpy.props.StringProperty(subtype="FILE_PATH")  # full path of selected item (path+filename)
        filename: bpy.props.StringProperty(subtype="FILE_NAME")  # name of selected item
        directory: bpy.props.StringProperty(subtype="FILE_PATH")  # directory of the selected item
        files: bpy.props.CollectionProperty(type=bpy.types.OperatorFileListElement)  # a collection containing all the selected items as filenames

        # Controls
        my_float1: bpy.props.FloatProperty(name="Scale", default=1.0, description="Changes Scale of the imported Mesh")
        my_bool1: bpy.props.BoolProperty(name="Clear Scene", default=True, description="Deletes everything in the scene prior to importing")

        # Runs when this class OPENS
        def invoke(self, context, event):

            # Retrieve Settings
            try: self.filepath = bpy.types.Scene.acecombat_ah_imp_filepath
            except: bpy.types.Scene.acecombat_ah_imp_filepath = bpy.props.StringProperty(subtype="FILE_PATH")

            try: self.directory = bpy.types.Scene.acecombat_ah_imp_directory
            except: bpy.types.Scene.acecombat_ah_imp_directory = bpy.props.StringProperty(subtype="FILE_PATH")

            try: self.my_float1 = bpy.types.Scene.acecombat_ah_imp_my_float1
            except: bpy.types.Scene.acecombat_ah_imp_my_float1 = bpy.props.FloatProperty(default=1.0)

            try: self.my_bool1 = bpy.types.Scene.acecombat_ah_imp_my_bool1
            except: bpy.types.Scene.acecombat_ah_imp_my_bool1 = bpy.props.BoolProperty(default=False)


            # Open File Browser
            # Set Properties of the File Browser
            context.window_manager.fileselect_add(self)
            context.area.tag_redraw()

            return {'RUNNING_MODAL'}

        # Runs when this Window is CANCELLED
        def cancel(self, context): print("run *SPAM*")

        # Runs when the class EXITS
        def execute(self, context):

            # Save Settings
            bpy.types.Scene.acecombat_ah_imp_filepath = self.filepath
            bpy.types.Scene.acecombat_ah_imp_directory = self.directory
            bpy.types.Scene.acecombat_ah_imp_my_float1 = self.my_float1
            bpy.types.Scene.acecombat_ah_imp_my_bool1 = self.my_bool1

            # Run Callback
            acecombat_ah_imp_callback(self.directory + "\\", self.files, self.my_bool1, self.my_float1)

            return {"FINISHED"}

            # Window Settings

        def draw(self, context):

            self.layout.row().label(text="Import Settings")

            self.layout.separator()
            self.layout.row().prop(self, "my_bool1")
            self.layout.row().prop(self, "my_float1")

            self.layout.separator()

            col = self.layout.row()
            col.alignment = 'RIGHT'
            col.label(text="  Author:", icon='QUESTION')
            col.alignment = 'LEFT'
            col.label(text="mariokart64n")

            col = self.layout.row()
            col.alignment = 'RIGHT'
            col.label(text="Release:", icon='GRIP')
            col.alignment = 'LEFT'
            col.label(text="March 23, 2021")

        def menu_func_import(self, context):
            self.layout.operator("importhelper.acecombat_ah_imp", text="Ace Combat Assault Horizon (*.fhm)")

    # Register Operator
    bpy.utils.register_class(ImportHelper_acecombat_ah_imp)
    bpy.types.TOPBAR_MT_file_import.append(ImportHelper_acecombat_ah_imp.menu_func_import)

    # Call ImportHelper
    bpy.ops.importhelper.acecombat_ah_imp('INVOKE_DEFAULT')



if not useOpenDialog:
    deleteScene(['MESH', 'ARMATURE'])  # Clear Scene
    clearListener()  # clears out console
    read(
        #"C:\\Users\\Corey\\Desktop\\AuditionOnlien\\nozomi\\model_id\\mech\\airp\\d_tnd4\\d_tnd4_pcom.fhm"
        #"C:\\Users\\Corey\\Desktop\\AuditionOnlien\\nozomi\\model_id\\mech\\airp\\d_tnd4\\d_tnd4_pcom\\d_tnd4_pcom.fhm_13.NDXR"
        "G:\\WA3_memory\\f16xl.ndp3"
        )
    messageBox("Done!")
else: acecombat_ah_imp(True)


Top
   
PostPosted: Thu Mar 25, 2021 11:09 am 

Joined: Wed Nov 02, 2016 4:15 am
Posts: 12
Alright, did some tests with the plugin, mostly i also upgrade to Blender 2.92, but seems the plugin also works fine with 2.90 too. mostly about the NDP3 imports, the most obvious thing i noticed is unlike the NDXR, they are imported without their name structure nor in separated collections, i could understand because of the difference of Big Endian to Little Endian. by far, some meshes seems to import really fine, some with missing elements like the nozzle of the F-16XL, but other suprising ones, like the weapon pylons and the machine gun SWP of ADA-01B(which are hidden by default at least on hangar modes), is interresing due on their current game, they use a same structure for storing separate meshes interconected, but mostly on ACAH and ACI is made by a FHM with groups of NDXR/NDP3, when on AC7 they separated the meshes on uassets instead. i noticed the UV maps seems to be missing too, despite with some leftovers, i noticed is also a side effect on the NDXR too, as some manages to get some maps intact and merged with other meshes which i noticed it could be separated ones, but other ones lack the coordinates, as you may read on Shatokay post, the UV for both are stored in half-float UV maps, seeing it was one of the coords i took a while to make it work manually, but then finally understood the structure.
Image
Image
Image
Image
Image

In general for the NDXR files, it turns to be a impressive work, despite mostly of the work was based on the o_xxxx and d_xxxx meshes(which checking by my eyes now, seems to be both CPU and damage models), trying to import the player models(p_xxxx) result in a gamble of parts by originally begin separated meshes with skeleton bone influences, not sure exactly if the ones of the player turns to be more complex of the CPU ones(despite the helos somehow got very intact), as i noticed they also are separated, and aren't in wrong axis either for instruments and the "steel carnage" divisions. i would say part of that turns the script was made on May 22, and updated on May 23, and implementing the correction for the player models would take more time for delievering the script.
Image
Image
Image
Image
Image
Image
Image
Image

Not only for the aircraft CPU models, but the script also works really good with the weapon props, ground and sea units too like vessels or SAMs(obvious exceptions turns into map chunks, human characters, and scenario props), which tends to work fine by them also using the NDXR structure.
Image
Image
Image
Image
Image
Image
Image

In general, even for the earlier stages, the script got huge potential, i know working at one script for all models won't work 100% at the time, as some specific models tend to glitch their pivot position without having them as reference, but mostly if you would still have interrest in updating it, adding the UV coords for the ACI dumps, fixing the wrong axis of both player models(even if the variable wings turns to be glitched but with plyons intact on the wing, is easy to fix their orientation by re-rigging the model), and doing corrections to optimize the glitched imports of some of the CPU models. would be a huge step for preservating and maybe porting those models into other games, or just using them as reference on Blender when making custom texture skins, due the UVs are a good advantage. aside of the ACAH ones, unless some models turns to be hard at fixing, i will be sending more ACI memory dumps for a better reference and also for further testing. as a bonus, i also included some samples of AC6 dumps too, their structure seems to be the same on the NDXR format basics, despite i can't import by certain differences it got by begin from an early version of the engine. so considering the way i dumped them is a bit different due i unsure which end point the main model files would end unlike the ACAH/ACI ones, but i know mostly general NDXR mesh data ends between the next header at times by grouping, but is a hottake in parts.


Attachments:
ACIdumpsplusAC6dumpsamples.7z [1.34 MiB]
Downloaded 13 times
Top
   
PostPosted: Thu Mar 25, 2021 11:44 am 
User avatar

Joined: Fri Aug 08, 2014 12:59 am
Posts: 8
yeah I noticed that the positions of some of the objects are bad, there is transform data for each mesh but I wasn't able to decode it. There seems to be 7 floats, so I thought maybe position X,Y,Z and then rotation X, Y, Z, W but it doesnt work

I was sort of hoping someone else knew about it, if you figure it out of course that'll be easy to implement into the script...

Also half floats are being used, but the kicker is that they use a flexible vertex format which means that some use half floats, and some don't... so I was only able to implement the FVF specs for the provided samples.

I will look at the new samples later and add support for those aswell and I'll double check why the names are not working... but again I can only work with what is provided to me


Top
   
PostPosted: Thu Mar 25, 2021 12:34 pm 

Joined: Wed Nov 02, 2016 4:15 am
Posts: 12
Thanks for the reply, yeah, i know some of the transformation axis would be managed with floats, i may try to check it if i could find them, but can't guarantee results too early, as it may take a while. also thanks for the information about the UV issues, i through by begin mostly UV coordinates, i didn't expected they would behave like that with the script, seeing the Hex2obj sample i did manages to import it fine, with the script import, i noticed the corrupted UV coords keeps into the shape of a polygonal cross, which was the previous results i was getting before by begin stuck of how the floats worked.
Image


Top
   
PostPosted: Thu Mar 25, 2021 12:38 pm 

Joined: Wed Nov 02, 2016 4:15 am
Posts: 12
And as a sort of correction of a errata i did, of course, if you did double checked during the script writing, the PS3 ones are the ones in Big Endian, when the AC6(i guess) and the ACAH ones are written in Big Endian


Top
   
PostPosted: Thu Mar 25, 2021 1:44 pm 
User avatar

Joined: Fri Aug 08, 2014 12:59 am
Posts: 8
the script can read either big or little endian, it shouldnt matter

Edit: in ACAH, the fhm is a file container, that holds multiple files in it which some are NDXR which are models.

But the files you sent are NDP3's which should be 1 model, however I notice with NDP3 you sent has multiple NDP3's inside of it self. Uhm its going to be hard to read those without the FHM file table, but I'll see what I can do


Top
   
PostPosted: Thu Mar 25, 2021 2:12 pm 

Joined: Wed Nov 02, 2016 4:15 am
Posts: 12
Yeah, mostly it was one of the differences i noticed, i know on PC, FHM got a sort of table of contents, but it wasn't something that i could managed to isolate easily, as some FHM headers were too short, along begin interconected with NTP3 texture headers, at least comparing even with the PS3 version of ACAH, i couldn't find the same headers, as sort if they didn't existed at first place.


Top
   
PostPosted: Thu Mar 25, 2021 5:38 pm 
User avatar

Joined: Fri Aug 08, 2014 12:59 am
Posts: 8
Change Log:
- Major update to work better with PS3 memory Dumps
- Added Scan option in import dialog to scan for any nested mesh blocks
- Added support for fvf type 7 (44 bytes per vertex)
- Added fix to read names from memory dumps

**script is attached below as a text file


Attachments:
read ndp3 and nxdr from ps3 mem dumps.txt [43.59 KiB]
Downloaded 16 times
Top
   
PostPosted: Tue Mar 30, 2021 3:29 am 

Joined: Wed Nov 02, 2016 4:15 am
Posts: 12
Hello once again, as i said, at some point i would be trying to research and parse the model for making some tests, despite it wasn't easier either as Saturday i went into a strong thunderstorm. i also managed to test some functions of the new version of the script in general. as a overall review, the file scan in fact turns to be useful when searching NDXR/NDP3 of Project Aces games it can be unsure if their structure is the same or not afterall. with the option toggled, the player models of ACAH somehow will be restricted at importing the cockpit mesh always, which can be fixed by toggling off the file scan, but still, for some mesh samples i dumped and tested, i also managed to make a better research from the overall fhm format. in fact, i was correct that the FHM serves more as a in-house container for storing multiple data, so the FHM on the model question works for structuring their data, the first bytes of the FHM NDXR containers i would say it works as a table of contents, despite unsure in fact, i know that the last time i tried porting the NDP3 files, the PC version of ACAH crashed and close by finding an incompability of those bytes not begin tweaked and endianess i would say. the general dump i did for the NDP3 files works at the same way as the ACAH rips except of the main FHM header, at the last mesh set, they will end with a sort of ID moniker indentification bytes(the plain p_f16x you may noticed at some), but that isn't the main ruleset for importing ND formats, in general, a FHM container with model parts for the player aircrafts would look more or less like this :

Code:
01 : Main mesh
02 : Cockpit mesh
03 : Exterior(Cockpit View)
04 : Nozzle mesh(Falken Z.O.E 'zoex' and Adler 'adlr' will include more meshes, which can be imported by the script, but one-mesh nozzles won't import, ex : f16xl)
05 : Landing Gear mesh
06 : Hangar placement dummy
*07-11 : dupes of the model order(secondary LOD mesh, as it got a few lower filesize and polycount, despite similar structure), main difference is the Exterior mesh includes the nozzle by default, along ACAH NDXR seems to lack the nozzle dupe, along it also have issues to import the main nozzle too
*Piston Aircraft(5 in total) will include 01-02 by default, but will be reduced to 03 and 05, having 4 mesh sets only, some uncoventional aircraft(X-49 and R-101) will lack some mesh groups also


So in general, searching for NDP3 on my dumps, or NDXR on the ACAH samples, you'll notice that following with the next search, you will get the next ND header below the last bits of the NU engine information like the NU_HASH and NU_FLAG's, so the end of those models obviously will go until the next ND header pops up. the way i split those for understanding better was thanks to a python unix script, it was a bit hard to make it to work due i using a windows system, and the script was made with linux in mind. but it was helpful to understand better how the structure of those meshes work, and begin suprised cockpit models are also preloaded, which i through they would be impossible, but partially i hoped they would work, seeing ACAH and ACI got their engine functions based on AC6, and on 6, they got a function for vieweing in cockpit view, so in parts, this data bytes exist still, despite not used on the hangars anymore. the only set of meshes i found issues at importing, are related to the "nozl" and "shnozl" sets(04 and 09), the only ones i managed to import, are the ones from Falken Z.O.E(zoex) and Adler(adlr), which by getting two pairs of trust vectoring nozzles, the script manages to import them without issues, which isn't the case of the nozzle of the F-16XL for example, which got simply one mesh influenciated by vertex morphs(i suppose they would be related with some of the KFM1 and MOP2 headers you may find). but yeah, unfortunately i don't got lucky either at getting those FHM headers, i may try once again seeing it was interresing to find out some models i was ripping wrong without knowing why, due trying to find them on the PC version seems to not work either, even by searching the exact 4 byte structures, but mostly implementing the way it imports the FHM files without scan(which imports each group in order, except for nozzle meshes) would work despite lacking this first section of bytes.
Image
Image
Image
Image
-----------------------------------------------------------------------------------
Image
Image

Right, going into the second topic, i fired up ACI to make some tests with the F-16XL model, mostly using CE for doing RTM edits due is mostly the tool i use for dumping the assets. from the tests i did by NOPing the values into 00, i could be probably wrong, but the down section of the NDXR file during offset (0x20) seems to be my main suspect, i know some other Bandai Namco games which makes use of the ND formats and NU Engine architetures in general, they could be using the structure of 3x4 or 4x4 matrices with inverted right hand coordinate system. the ND formats in general they include skeletons by default, but mostly isn't something i could look seeing isn't a priority on hex2obj, but by NOPing this section, i seem to suspect they may include relevant data for the bones, as some manages the parenting of them on the sample pictures. on my memory dumps also, aside of NDP3 models, i dumped also some files with the MNT header, and some of them make callbacks to bone structures. but yeah, NOPing the area of the edges would result in vertex bomb exploding, and NOPing the vertex area would make them disapear slowly, with the tidbit you mentioned of dynamic half floats, that one section will simply result in UV corruptions.
Image
Image
Image

the last topic, mostly begin minimum stuff, would be the reminder that some of the nozzle meshes isn't begin imported with the script currently, the AC6 rips i did before, mostly of Gyges and F-16, i did by mistake of sending simple parts of them only, seeing i managed to understood their structure better with the ND formats in general, they wouldn't be the issue due i can import them without hassle. however there's a model englobed into the map props that isn't working with the script either, the mesh in question, included on this sample pack, is from a cruise missile warhead called Stauros, which is fired by a huge electromagnetic ground railgun called "Chandelier" on the universe of AC6, during ripping the missiles only, i noticed the warhead shell was refered as a map prop, due the eml on the name gave me the explicit clue, and i found out strange due is originally a air-to-air/weaponry prop, but it was begin refered as a map prop instead. so if you could add at least the provisory support to the layout of those meshes it would be helpful too, there are also some map props and objects i would have interrest, not sure if patching with the Stauros dump would be enough for making them compatible, but seeing ACI is impossible to play in-game, the only ground mesh i could be ripping at some point would be Chandelier itself. but yeah, that's everything from my infodump today, looking forward for the future improvements of the script, despite of course, research would be necessary, which i'll be seeking at doing when possible, due i got my occupations also.
Image
Image


Attachments:
samplesmar30.7z [11.94 KiB]
Downloaded 12 times
Top
   
PostPosted: Tue Mar 30, 2021 2:55 pm 
User avatar

Joined: Fri Aug 08, 2014 12:59 am
Posts: 8
Hello, I'm not sure what's happening with the positions or bones, or animation data as that is outside my expertise..

However I have looked at the other issues, here is the change log

Quote:
2021-03-30
fixed mesh import function to ignore missing UV's to import mesh
byte used for vertex stride failed to work on 'stauroswarhead.fhm' As a work-around a 'Guess Vertex Stride' option was added to the dialog



in accordance with the second issue which is that the vertex stride is incorrect and makes the mesh appear corrupt;
that is because of the byte I am using to identify the different mesh types seems to not be working with the one sample you have provided. Without digging deeper into it, I have just added a simple work-around (which is not full proof) but does work for the one sample you have provided.

This here is the struct of the mesh information in the binary, the variable 'unk036' is currently being used to identify the vertex definition type. this may determine if the mesh contains UV's or normals, bones, weights etc...

Code:
struct mesh_info {
   uint32_t   face_addr
   uint32_t   vert_addr
   uint32_t   unk033
   uint16_t   vert_count
   uint8_t    unk036   // ? Vertex Definition Type
   uint8_t    unk037
   uint32_t   cmd_addr
   uint32_t   unk038
   uint32_t   unk039
   uint32_t   unk040
   uint16_t   face_count
   uint16_t   unk042
   uint32_t   unk043
   uint32_t   unk044
   uint32_t   unk045
};


however it doesn't to hold up against the one sample so another byte may control it, or possible we just need another strategy....

regardless a 'Guess Vertex Stride' was added to the import dialog, and should work on your 'stauroswarhead.fhm' sample
Attachment:
read ndp3 and nxdr from ps3 mem dumps v2.txt [44.75 KiB]
Downloaded 10 times


Top
   
Display posts from previous:  Sort by  
Post new topic  Reply to topic  [ 12 posts ] 

All times are UTC


You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum
You cannot post attachments in this forum

Search for:
Powered by phpBB® Forum Software © phpBB Limited