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 }