art3de/pkg/assets/res_format.go

214 lines
5.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}