214 lines
5.6 KiB
Go
214 lines
5.6 KiB
Go
package assets
|
||
|
||
import (
|
||
"encoding/binary"
|
||
"fmt"
|
||
"io"
|
||
"os"
|
||
"path/filepath"
|
||
"time"
|
||
)
|
||
|
||
// RESHeader зоголовок оригинального файла
|
||
type RESHeader struct {
|
||
Magic [4]byte // 0x3C, 0xE2, 0x9C, 0x01
|
||
FilesCount uint32
|
||
FileTableOffset uint32
|
||
NamesSize uint32
|
||
}
|
||
|
||
// RESFileRecord запись файла в оригинальном RES
|
||
type RESFileRecord struct {
|
||
NextIndex int32
|
||
FileSize uint32
|
||
FileOffset uint32
|
||
LastChange uint32
|
||
NameLen uint16
|
||
NameOffset uint32
|
||
}
|
||
|
||
// RESFile информация о файле в RES
|
||
type RESFile struct {
|
||
Name string
|
||
Size uint32
|
||
Offset uint32
|
||
LastChange time.Time
|
||
NameLen uint16 // Добавим для корректной работы
|
||
}
|
||
|
||
// RESArchive представляет оригинальный RES архив
|
||
type RESArchive struct {
|
||
File *os.File
|
||
Header RESHeader
|
||
Files []RESFile
|
||
isOpen bool
|
||
}
|
||
|
||
// OpenRES открывает оригинальный RES файл
|
||
func OpenRES(path string) (*RESArchive, error) {
|
||
file, err := os.Open(path)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("failed to open RES file: %w", err)
|
||
}
|
||
|
||
archive := &RESArchive{
|
||
File: file,
|
||
isOpen: true,
|
||
}
|
||
|
||
// Читаем заголовок
|
||
if err = binary.Read(file, binary.LittleEndian, &archive.Header); err != nil {
|
||
_ = file.Close()
|
||
return nil, fmt.Errorf("failed to read RES header: %w", err)
|
||
}
|
||
|
||
// Проверяем магиеские байты
|
||
if archive.Header.Magic != [4]byte{0x3C, 0xE2, 0x9C, 0x01} {
|
||
_ = file.Close()
|
||
return nil, fmt.Errorf("invalid RES file (wrong magic)")
|
||
}
|
||
|
||
// Читаем таблицу файлов
|
||
if err = archive.readFileTable(); err != nil {
|
||
_ = file.Close()
|
||
return nil, err
|
||
}
|
||
|
||
return archive, nil
|
||
}
|
||
|
||
// readFileTable читает таблицу файлов RES
|
||
func (ra *RESArchive) readFileTable() error {
|
||
// Преходим к таблице файлов
|
||
if _, err := ra.File.Seek(int64(ra.Header.FileTableOffset), io.SeekStart); err != nil {
|
||
return err
|
||
}
|
||
|
||
// Сначала читаем все записи файлов
|
||
records := make([]RESFileRecord, ra.Header.FilesCount)
|
||
for i := uint32(0); i < ra.Header.FilesCount; i++ {
|
||
var record RESFileRecord
|
||
if err := binary.Read(ra.File, binary.LittleEndian, &record); err != nil {
|
||
return fmt.Errorf("failed to read file record %d: %w", i, err)
|
||
}
|
||
records[i] = record
|
||
}
|
||
|
||
// Вычисляем смещение таблицы имен
|
||
nameTableOffset := ra.Header.FileTableOffset + 22*ra.Header.FilesCount
|
||
ra.Files = make([]RESFile, ra.Header.FilesCount)
|
||
|
||
// Читаем имена файлов и создаем записи
|
||
for i, record := range records {
|
||
// Переходим к имени файла
|
||
nameOffset := nameTableOffset + record.NameOffset
|
||
if _, err := ra.File.Seek(int64(nameOffset), io.SeekStart); err != nil {
|
||
return fmt.Errorf("failed to seek to name offset: %w", err)
|
||
}
|
||
|
||
// Читаем имя в CP1251
|
||
nameBytes := make([]byte, record.NameLen)
|
||
if _, err := io.ReadFull(ra.File, nameBytes); err != nil {
|
||
return fmt.Errorf("failed to read name: %w", err)
|
||
}
|
||
|
||
// Конвертируем из CP1251 в UTF-8
|
||
name := decodeCP1251(nameBytes)
|
||
|
||
// Создаем запись файла
|
||
ra.Files[i] = RESFile{
|
||
Name: name,
|
||
Size: record.FileSize,
|
||
Offset: record.FileOffset,
|
||
LastChange: time.Unix(int64(record.LastChange), 0),
|
||
NameLen: record.NameLen,
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// GetFile получает файл из архива
|
||
func (ra *RESArchive) GetFile(name string) ([]byte, error) {
|
||
for _, file := range ra.Files {
|
||
if file.Name == name {
|
||
return ra.readFileData(file.Offset, file.Size)
|
||
}
|
||
}
|
||
return nil, fmt.Errorf("file not found: %s", name)
|
||
}
|
||
|
||
// GetFileByIndex получает файл по индексу
|
||
func (ra *RESArchive) GetFileByIndex(index int) ([]byte, error) {
|
||
if index < 0 || index >= len(ra.Files) {
|
||
return nil, fmt.Errorf("invalid file index: %d", index)
|
||
}
|
||
|
||
file := ra.Files[index]
|
||
return ra.readFileData(file.Offset, file.Size)
|
||
}
|
||
|
||
// readFileData читает данные файла
|
||
func (ra *RESArchive) readFileData(offset, size uint32) ([]byte, error) {
|
||
if _, err := ra.File.Seek(int64(offset), io.SeekStart); err != nil {
|
||
return nil, fmt.Errorf("failed to seek to file data: %w", err)
|
||
}
|
||
|
||
data := make([]byte, size)
|
||
if _, err := io.ReadFull(ra.File, data); err != nil {
|
||
return nil, fmt.Errorf("failed to read file data: %w", err)
|
||
}
|
||
|
||
return data, nil
|
||
}
|
||
|
||
// ListFiles возвращает список файлов
|
||
func (ra *RESArchive) ListFiles() []string {
|
||
names := make([]string, len(ra.Files))
|
||
for i, file := range ra.Files {
|
||
names[i] = file.Name
|
||
}
|
||
return names
|
||
}
|
||
|
||
// GetFileInfo возвращает информацию о файле
|
||
func (ra *RESArchive) GetFileInfo(name string) (*RESFile, error) {
|
||
for _, file := range ra.Files {
|
||
if file.Name == name {
|
||
return &file, nil
|
||
}
|
||
}
|
||
return nil, fmt.Errorf("file not found: %s", name)
|
||
}
|
||
|
||
// ExtractAll извлекает все файлы
|
||
func (ra *RESArchive) ExtractAll(outputDir string) error {
|
||
for _, file := range ra.Files {
|
||
data, err := ra.GetFile(file.Name)
|
||
if err != nil {
|
||
return fmt.Errorf("failed to extract %s: %w", file.Name, err)
|
||
}
|
||
|
||
outputPath := filepath.Join(outputDir, file.Name)
|
||
if err = os.MkdirAll(filepath.Dir(outputPath), 0755); err != nil {
|
||
return err
|
||
}
|
||
|
||
if err = os.WriteFile(outputPath, data, 0644); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// Close закрывает архив
|
||
func (ra *RESArchive) Close() error {
|
||
if ra.isOpen {
|
||
ra.isOpen = false
|
||
return ra.File.Close()
|
||
}
|
||
|
||
return nil
|
||
}
|