Sources.RU Magazine Поиск по журналу
 

Ссылки

Делаем свой "Pak-explorer"

Автор: Seriy-Coder

Предисловие

Статья наша будет посвящена такому небезызвестному формату файлов как .pak. Собственно, этот формат был придуман id Software™ еще в далеком 1996 году, когда они разрабатывали движок игры. Файлы данного формата представляют собой некий пакет файлов. Упаковка файлов, включаемых в .pak, отсутствует. Разрабатывался формат для легкого и быстрого доступа из движка игры к необходимым файлам, находящимся «снаружи». Должен сказать, что он используется не только в Quake I, он нашел свое применение и в Quake II, Half-Life и некоторых других играх, сделанных на основе движка Quark.
После, в 1997 году, была написана программа под названием Pak Explorer. Она позволяла просматривать и модифицировать содержимое .pak файлов прямо внутри редактора.


А чем же займемся мы с вами? А займемся мы созданием программы под названием «Pak-Explorer Ex» (от слова Extended – расширенный).

Основы.

Перво-наперво мы определим, какие функции должна выполнять наша программа. Естественно, раз уж мы задались целью написать расширенный Pak-Explorer, необходимо сначала реализовать функции оригинальной программы, а именно:


  • Открывать и создавать .pak файлы
  • Редактировать .pak файлы (удалять\добавлять файлы)
  • "Быстро" просматривать файлы в .pak файле (например, проигрывать wav-файлы прямо в программе)
  • Обеспечивать поддержку Drag’n’Drop

  • А теперь определим множество расширенных функций:


  • Синхронизация (если изменяются оригиналы файлов в папке, программа предлагает заменить их в .pak файле)
  • Возможность удалять «в корзину» (иногда это бывает полезно, если вы вдруг удалили не тот файл, а он был в единственном экземпляре)
  • Поддержка отмены выполненных действий (Ctrl+Z)

  • С функциями разобрались. Теперь приступим к реализации.

    Запускаем Visual Basic. В меню выбора проекта выбираем Standart Exe. Подключаем к проекту следующие контролы: Microsoft Common Dialog Control 6.0 и Microsoft Windows Common Controls 6.0 (правый щелчок мыши на панели инструментов toolbox, пункт меню Components). Размещаем на форме компоненты TreeView, ListView, ToolBar, ImageList и CommonDialog как показано на рисунке. Дерево каталогов принято размещать слева (для простоты навигации), хотя вы можете разместить его так, как вам будет угодно. Имена объектов также указаны на рисунке:

    Далее следует таблица для создания меню. Для наглядности и удобства принято создавать меню в подобном стиле. Вы можете заметить, что практически во всех программах пункт меню, обозначенный как Edit, отвечает за редактирование, а его подменю Paste за вставку, например, текста из буфера обмена. Я тоже постарался следовать этому негласному правилу. Единственное отличие, которое вы увидите – это подменю Sinchronize Files меню Edit. Оно будет отвечать за синхронизацию файлов в .pak файле и в директории (например, заменять старые файлы на новые), и кажется логичным поместить его именно в это меню.


    Таблица 1. Элементы меню.
     Caption (Заголовок эл-та меню)  Name (Имя эл-та меню) 
    &FilemnuFile
    Подменю &New PakmnuNew
    Подменю &Open PakmnuOpen
    Подменю &Save PakmnuSave
    Подменю Save Pak &As...mnuSaveAs
    Подменю -Sep1
    Подменю &QuitmnuExit
    &EditmnuEdit
    Подменю Co&pymnuCopy
    Подменю C&utmnuCut
    Подменю &PastemnuPaste
    Подменю &DeletemnuDel
    Подменю -Sep2
    Подменю S&inchronize FilesmnuSinc

    Для красивого выравнивания объектов на форме при изменениях размера формы потребуется установить у объекта Toolbar свойство Align = 1 (vbAlignTop). У остальных видимых объектов свойство Align отсутствует, поэтому необходимо написать код, который будет их выравнивать. Код этот помещается в обработчик события Form_Resize(), которое происходит при изменении размера формы.

    On Error Resume Next
    Tree1.Move 0, tbr1.Height, 1935, ScaleHeight - tbr1.Height
    List1.Move Tree1.Width + 100, tbr1.Height, ScaleWidth - (Tree1.Width + 100), _
    ScaleHeight - tbr1.Height
    

    Первая строка «защищает» от ошибки, происходящей при сильном уменьшении размеров формы (когда высота формы меньше высоты ToolBar’a). Дерево каталогов размещается ниже панели инструментов ровно на ее высоту, слева отступа нет, в высоту растягиваем на высоту формы минус высоту панели инструментов. Единственная постоянная величина – ширина дерева. Лист размещается на той же высоте, с отступом влево на ширину дерева, а ширину листа вычисляем путем вычитания ширины дерева из ширины формы. Высота аналогична высоте дерева (вычисляется тем же способом).


    Теперь я опишу формат самого файла, с которым предстоит работать. Вначале идет заголовок (12 байт), который состоит из трех полей по 4 байта. Первое поле содержит строку PACK, идентифицирующую данный формат (ее будем использовать для проверки открываемых файлов на правильность). Следующие четыре байта – это смещение каталога ресурсов относительно Pak-файла, а последние четыре – размер каталога в записях (то есть, количество ресурсов). Вот как это выглядит на VB:

    Private Type pakheader_t
        magic       As String * 4   ‘ заголовок PACK
        diroffset   As Long         ‘ смещение каталога ресурсов
        dirsize     As Long         ‘ размер каталога
    End Type
    

    Сам каталог всегда находится в конце файла. Формат каждой записи следующий (каждая запись представляет файл, который содержится в .pak файле):

    Private Type pakentry_t
        filename    As String * 56	‘ имя записи (имя и путь к файлу)
        offset      As Long          	‘ смещение (ГДЕ находятся данные)
        size   As Long             	‘ размер данных (размер файла)
    End Type

    Поле filename содержит имя файла, включая путь к нему. Следующий параметр offset указывает смещение ресурса относительно начала Pak-файла. А параметр size указывает размер этого ресурса.


    Итак, теперь есть типы, с помощью которых можно работать с .pak файлами. Приступим к реализации первой возможности программы, а именно, к возможности:


  • Открывать и создавать .pak файлы
  • Начнем с реализации возможности создания .pak файла.
    Создадим новый модуль (Project->AddModule), назовем его mMain, и будем помещать в него основные функции.

    Конкретно за создание будет отвечать одна маленькая функция:

    Public Function CreatePAK(ByVal filename As String) As Boolean
    Dim FFe As Integer
    
    FFe = FreeFile
    PakHeader.magic = "PACK"
    PakHeader.diroffset = 12
    PakHeader.dirsize = 0
    
    On Error GoTo err_h
    If Dir(filename, vbNormal) <> "" Then Kill filename
    Open filename For Binary As #FFe Len = Len(PakHeader)
        Put #FFe, , PakHeader
    Close #FFe
    
    CreatePAK = True
    Exit Function
    err_h:
    CreatePAK = False
    End Function

    Передаваемый параметр – это есть полное имя и путь к .pak файлу (например “c:\newpack.pak”). Заполняем структуру PakHeader (о которой говорилось чуть выше) «правильными» значениями, а именно: заголовок “PACK”, смещение = 12 (так как сам заголовок «весит» 12 байт), размер каталога = 0 (потому что ничего там ещё нет). Проверяем, нету ли такого же файла, а если есть – удаляем его. Открываем файл для двоичного (Binary) доступа и смело пишем всю структуру PakHeader в него. Все. Закрываем файл, функция возвращает True.

    Пойдем дальше. Надо уметь открывать файлы формата .pak…

    Public Function OpenPAK(ByVal filename As String) As Boolean
    Dim FFe As Integer, loopvar As Integer, curentry As Integer
    Dim ReadString As String * 64
    FFe = FreeFile
    Open filename For Random As #FFe Len = Len(PakHeader)
    Get #FFe, , PakHeader
    Close #FFe
    If PakHeader.magic <> "PACK" Then
        FFe = MsgBox("Invalid Pack File", vbOKOnly + vbInformation)
        OpenPAK = False
        Exit Function
    End If
    
    ReDim Preserve StoreEntry((PakHeader.dirsize / 64))
    FFe = FreeFile
    curentry = 1
    Open filename For Binary As #FFe
    Get #FFe, PakHeader.diroffset + 1, PakEntry
    For loopvar = 1 To ((PakHeader.dirsize / 64))
        StoreEntry(curentry).filename = PakEntry.filename
        StoreEntry(curentry).offset = PakEntry.offset
        StoreEntry(curentry).size = PakEntry.size
        curentry = curentry + 1
        Get #FFe, , PakEntry
    Next
    Close #FFe
    orig_filename = filename
    OpenPAK = True
    End Function

    Эта функция должна сделать 2 вещи: проверить, правильный ли файл открывается, а если да, то заполнить массив StoreEntry(), который-то и хранит данные о файлах в .pak файле. Итак, для начала читаем заголовок (PakHeader). С помощью инструкции Get #FFe, PakHeader заполняем структуру данными, а затем проверяем PakHeader.magic, соответствует ли файл формату "PACK". Если нет, говорим, что формат неправильный и выходим из функции. Если формат правильный, то заполняем массив StoreEntry данными о находящихся в .pak’e файлах:

    ReDim Preserve StoreEntry((PakHeader.dirsize / 64))

    Переопределяем массив согласно количеству реально находящихся файлов в .pak’e:

    Open filename For Binary As #FFe
    Get #FFe, PakHeader.diroffset + 1, PakEntry
    

    Открываем повторно файл, считываем первое «вхождение».

    For loopvar = 1 To ((PakHeader.dirsize / 64))
        StoreEntry(curentry).filename = PakEntry.filename
        StoreEntry(curentry).offset = PakEntry.offset
        StoreEntry(curentry).size = PakEntry.size
        curentry = curentry + 1
        Get #FFe, , PakEntry
    Next

    Теперь бегаем по всем «вхождениям», считываем их и соответственно заполняем поля массива данными. Вот и все. Считали.

    Остальные функции используем для заполнения данными TreeView и ListView.
    Следующие процедуры хорошо откомментированы, советую с ними ознакомиться самостоятельно:

    Процедура заполнения дерева каталогов:

    Sub ShowEntry(TV As TreeView)
      On Error Resume Next
      Dim TP2() As String
      Dim i&, j&
      Dim TMP$
      Dim ParentKey$
    
    TV.Nodes.Add , tvwFirst, "ROOT", orig_filename, 3
    ' Добавляем самый верхний, корневой элемент с именем открываемого .pak файла
      ' Бегаем по всем файлам в .pak файле
      For i = 0 To UBound(StoreEntry)
        ' Берем только путь, отделяя имя файла
        TMP = GetPath(StoreEntry(i).filename)
        ' если в строке есть символ «/», значит, имеем несколько вложенных папок
        If InStr(TMP, "/") > 0 Then
          ' разбиваем на подпапки
          TP2 = Split(TMP, "/")
          ParentKey = vbNullString
          ' добавляем первую
          TV.Nodes.Add "ROOT", tvwChild, TP2(0), TP2(0), 1, 2
          ' запоминаем предыдущую папку
          ParentKey = TP2(0)
          For j = 1 To UBound(TP2)
            TMP = TP2(j)
            ' «вкладываем» папку в предыдущую
            TV.Nodes.Add ParentKey, tvwChild, ParentKey & TMP & "\", TMP, 1, 2
            ParentKey = ParentKey & TMP & "\"
          Next
        Else
        ' не имеем вложенных, добавляем к корневой
          If TMP <> vbNullString Then TV.Nodes.Add "ROOT", tvwChild, TMP, TMP, 1, 2
        End If
      Next
    End Sub

    Процедура показа файлов в листе. Передаваемый параметр должен содержать путь внутри .pak файла.

    Public Sub ShowFilesEntry(ByVal path As String)
    Dim i As Long, l As Long, TMP As String
    fMain.List1.ListItems.Clear
    ' если длина пути НЕ равна длине оригинального пути имени файла (эта проверка
    ' необходима, так как корневой каталог в дереве TreeView назван
    ' оригинальным именем файла)
    
    If Len(path) <> Len(orig_filename) Then
        ' «отделяем» оригинальный путь и имя файла от нужного
        path = Right$(path, Len(path) - (Len(orig_filename) + 1))
        ' теперь «пробегаем» по всем «файлам» в .pak файле
        For i = LBound(StoreEntry) + 1 To UBound(StoreEntry)
        ' если в имени файла текущего StoreEntry встречается наш путь, то
        If InStr(1, StoreEntry(i).filename, path) <> 0 Then
            ' проверяем еще раз, на всякий случай, по длине
            If Len(GetPath(RemoveStuff(StoreEntry(i).filename))) = Len(path) Then
                ' добавляем в лист файл
                AddFile GetNam(StoreEntry(i).filename), _
                StoreEntry(i).size
            End If
        End If
        Next i
    Else
        ' теперь «пробегаем» по всем «файлам» в .pak файле
        For i = LBound(StoreEntry) + 1 To UBound(StoreEntry)
        ' если в имени файла не содержится обратных слешей, значит,
        ' файл лежит в корневой
    директории)
        If InStr(1, RemoveStuff(StoreEntry(i).filename), "/") = 0 Then
           ' опять проверяем на длину и добавляем в список
                AddFile RemoveStuff((StoreEntry(i).filename)), _
                StoreEntry(i).size
        End If
        Next i
    End If
    End Sub

    Данная процедура добавляет элемент в List:

    ' тут просто добавляем в лист имена файлов и их размер
    Private Sub AddFile(ByVal file As String, ByVal size As Long)
        fMain.List1.ListItems.Add , , file
    ' для удобства вычисляем размер файлов и показываем единицу размерности (Kb или b)
    If size <= 1024 Then
        fMain.List1.ListItems(fMain.List1.ListItems.Count) _
        .ListSubItems.Add , , size & " b"
    ElseIf size > 1024 Then
        fMain.List1.ListItems(fMain.List1.ListItems.Count) _
        .ListSubItems.Add  , , size \ 1024 & " Kb"
    End If
    End Sub

    Ну вот, теперь программа может открывать .pak файлы и просматривать их содержимое. В следующей статье добавим возможность редактировать (то есть, удалять\добавлять) .pak файлы.



    Скачать исходник: pak.zip (10 кб)

    С уважением, Seriy-Coder!



     Desingn by Шишкин Алексей (Лёха).
     ©2004-2008 by sources.ru.