Adding modding folder with a hacked build.
This commit is contained in:
@@ -0,0 +1,256 @@
|
||||
package lgres
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1"
|
||||
"github.com/inkyblackness/hacked/ss1/resource"
|
||||
"github.com/inkyblackness/hacked/ss1/resource/lgres/internal/compression"
|
||||
"github.com/inkyblackness/hacked/ss1/resource/lgres/internal/format"
|
||||
"github.com/inkyblackness/hacked/ss1/serial"
|
||||
)
|
||||
|
||||
// Reader provides methods to extract resource data from a serialized form.
|
||||
// Resources may be accessed out of sequence due to the nature of the underlying io.ReaderAt.
|
||||
type Reader struct {
|
||||
source io.ReaderAt
|
||||
firstResourceOffset uint32
|
||||
directoryOffset uint32
|
||||
directory []resourceDirectoryEntry
|
||||
|
||||
cache map[uint16]resource.View
|
||||
}
|
||||
|
||||
const (
|
||||
// ErrSourceNil is returned if the source is not valid.
|
||||
ErrSourceNil ss1.StringError = "source is nil"
|
||||
|
||||
// ErrFormatMismatch is returned if the format is unknown.
|
||||
ErrFormatMismatch ss1.StringError = "format mismatch"
|
||||
)
|
||||
|
||||
// ReaderFrom accesses the provided source and creates a new Reader instance
|
||||
// from it.
|
||||
// Should the provided decoder not follow the resource file format, an error
|
||||
// is returned.
|
||||
func ReaderFrom(source io.ReaderAt) (reader *Reader, err error) {
|
||||
if source == nil {
|
||||
return nil, ErrSourceNil
|
||||
}
|
||||
|
||||
var dirOffset uint32
|
||||
dirOffset, err = readAndVerifyHeader(io.NewSectionReader(source, 0, format.ResourceDirectoryFileOffsetPos+4))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
firstResourceOffset, directory, err := readDirectoryAt(dirOffset, source)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reader = &Reader{
|
||||
source: source,
|
||||
firstResourceOffset: firstResourceOffset,
|
||||
directoryOffset: dirOffset,
|
||||
directory: directory,
|
||||
cache: make(map[uint16]resource.View)}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// IDs returns the resource identifier available via this reader.
|
||||
// The order in the slice is the same as in the underlying serialized form.
|
||||
func (reader *Reader) IDs() []resource.ID {
|
||||
ids := make([]resource.ID, len(reader.directory))
|
||||
for index, entry := range reader.directory {
|
||||
ids[index] = resource.ID(entry.ID)
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
// View returns a reader prepared to extract data for the specified resource.
|
||||
// An error is returned if either the ID is not known, or the resource could not be prepared.
|
||||
func (reader *Reader) View(id resource.ID) (retrievedResource resource.View, err error) {
|
||||
if cachedResource, existing := reader.cache[id.Value()]; existing {
|
||||
return cachedResource, nil
|
||||
}
|
||||
resourceStartOffset, entry := reader.findEntry(id.Value())
|
||||
if entry == nil {
|
||||
return nil, resource.ErrNotFound(id)
|
||||
}
|
||||
resourceType := entry.resourceType()
|
||||
compressed := (resourceType & format.ResourceTypeFlagCompressed) != 0
|
||||
compound := (resourceType & format.ResourceTypeFlagCompound) != 0
|
||||
contentType := resource.ContentType(entry.contentType())
|
||||
|
||||
if compound {
|
||||
retrievedResource, err = reader.newCompoundResourceReader(entry, contentType, compressed, resourceStartOffset)
|
||||
} else {
|
||||
retrievedResource, err = reader.newSingleBlockResourceReader(entry, contentType, compressed, resourceStartOffset)
|
||||
}
|
||||
if err == nil {
|
||||
reader.cache[id.Value()] = retrievedResource
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func readAndVerifyHeader(source io.ReadSeeker) (dirOffset uint32, err error) {
|
||||
coder := serial.NewPositioningDecoder(source)
|
||||
data := make([]byte, format.ResourceDirectoryFileOffsetPos)
|
||||
coder.Code(data)
|
||||
coder.Code(&dirOffset)
|
||||
|
||||
expected := make([]byte, len(format.HeaderString)+1)
|
||||
for index, r := range format.HeaderString {
|
||||
expected[index] = byte(r)
|
||||
}
|
||||
expected[len(format.HeaderString)] = format.CommentTerminator
|
||||
if !bytes.Equal(data[:len(expected)], expected) {
|
||||
return 0, ErrFormatMismatch
|
||||
}
|
||||
|
||||
return dirOffset, coder.FirstError()
|
||||
}
|
||||
|
||||
func readDirectoryAt(dirOffset uint32, source io.ReaderAt) (firstResourceOffset uint32, directory []resourceDirectoryEntry, err error) {
|
||||
var header resourceDirectoryHeader
|
||||
headerSize := int64(binary.Size(&header))
|
||||
{
|
||||
headerCoder := serial.NewDecoder(io.NewSectionReader(source, int64(dirOffset), headerSize))
|
||||
headerCoder.Code(&header)
|
||||
if headerCoder.FirstError() != nil {
|
||||
return 0, nil, headerCoder.FirstError()
|
||||
}
|
||||
}
|
||||
|
||||
firstResourceOffset = header.FirstResourceOffset
|
||||
directory = make([]resourceDirectoryEntry, header.ResourceCount)
|
||||
if header.ResourceCount > 0 {
|
||||
listCoder := serial.NewDecoder(io.NewSectionReader(source, int64(dirOffset)+headerSize, int64(binary.Size(directory))))
|
||||
listCoder.Code(directory)
|
||||
err = listCoder.FirstError()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (reader *Reader) findEntry(id uint16) (startOffset uint32, entry *resourceDirectoryEntry) {
|
||||
startOffset = reader.firstResourceOffset
|
||||
for index := 0; (index < len(reader.directory)) && (entry == nil); index++ {
|
||||
cur := &reader.directory[index]
|
||||
if cur.ID == id {
|
||||
entry = cur
|
||||
} else {
|
||||
startOffset += cur.packedLength()
|
||||
startOffset += (format.BoundarySize - (startOffset % format.BoundarySize)) % format.BoundarySize
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type blockListEntry struct {
|
||||
start uint32
|
||||
size uint32
|
||||
}
|
||||
|
||||
func (reader *Reader) newCompoundResourceReader(entry *resourceDirectoryEntry,
|
||||
contentType resource.ContentType, compressed bool, resourceStartOffset uint32) (resource.View, error) {
|
||||
resourceDataReader := io.NewSectionReader(reader.source, int64(resourceStartOffset), int64(entry.packedLength()))
|
||||
|
||||
firstBlockOffset, blockList, err := reader.readBlockList(resourceDataReader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
blockCount := len(blockList)
|
||||
|
||||
rawBlockDataReader := io.NewSectionReader(resourceDataReader, int64(firstBlockOffset), resourceDataReader.Size()-int64(firstBlockOffset))
|
||||
var uncompressedReader io.ReaderAt
|
||||
if !compressed {
|
||||
uncompressedReader = rawBlockDataReader
|
||||
}
|
||||
|
||||
blockFunc := func(index int) (io.Reader, error) {
|
||||
if (index < 0) || (index >= blockCount) {
|
||||
return nil, resource.ErrBlockNotFound(index, blockCount)
|
||||
}
|
||||
|
||||
if compressed && (uncompressedReader == nil) {
|
||||
decompressor := compression.NewDecompressor(rawBlockDataReader)
|
||||
decompressedData, err := ioutil.ReadAll(decompressor)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uncompressedReader = bytes.NewReader(decompressedData)
|
||||
}
|
||||
|
||||
entry := blockList[index]
|
||||
reader := io.NewSectionReader(uncompressedReader, int64(entry.start)-int64(firstBlockOffset), int64(entry.size))
|
||||
return reader, nil
|
||||
}
|
||||
|
||||
return &readerResource{
|
||||
Properties: resource.Properties{
|
||||
Compound: true,
|
||||
ContentType: contentType,
|
||||
Compressed: compressed,
|
||||
},
|
||||
blockReader: blockReader{blockCount: len(blockList), blockFunc: blockFunc}}, nil
|
||||
}
|
||||
|
||||
func (reader *Reader) readBlockList(source io.Reader) (uint32, []blockListEntry, error) {
|
||||
listDecoder := serial.NewDecoder(source)
|
||||
var blockCount uint16
|
||||
listDecoder.Code(&blockCount)
|
||||
var firstBlockOffset uint32
|
||||
listDecoder.Code(&firstBlockOffset)
|
||||
lastBlockEndOffset := firstBlockOffset
|
||||
blockList := make([]blockListEntry, blockCount)
|
||||
for blockIndex := uint16(0); blockIndex < blockCount; blockIndex++ {
|
||||
var endOffset uint32
|
||||
listDecoder.Code(&endOffset)
|
||||
blockList[blockIndex].start = lastBlockEndOffset
|
||||
blockList[blockIndex].size = endOffset - lastBlockEndOffset
|
||||
lastBlockEndOffset = endOffset
|
||||
}
|
||||
|
||||
return firstBlockOffset, blockList, listDecoder.FirstError()
|
||||
}
|
||||
|
||||
func (reader *Reader) newSingleBlockResourceReader(entry *resourceDirectoryEntry,
|
||||
contentType resource.ContentType, compressed bool, resourceStartOffset uint32) (resource.View, error) {
|
||||
blockFunc := func(index int) (io.Reader, error) {
|
||||
if index != 0 {
|
||||
return nil, resource.ErrBlockNotFound(index, 1)
|
||||
}
|
||||
resourceSize := entry.packedLength()
|
||||
// The following check is a hack to re-create the behaviour of the engine.
|
||||
// While in truth the resource should only cover what its entry needs, Movie resources
|
||||
// can get larger than the 24bit size-field would allow (16MiB).
|
||||
// Interestingly, the engine does not limit this and simply reads beyond the limit.
|
||||
// In order to support such "broken" files, the behaviour is re-created here. This is mainly to
|
||||
// still allow creating such files and also give the editor a chance to work (and display such a case)
|
||||
// without completely having to rework the whole architecture here.
|
||||
// As audio of logs is also stored with this content type, limit to only single-resource files
|
||||
// to avoid a sudden increase of other resources in the file.
|
||||
isCutscene := (resource.ContentType(entry.contentType()) == resource.Movie) && (len(reader.directory) == 1)
|
||||
if isCutscene && (resourceStartOffset < reader.directoryOffset) {
|
||||
resourceSize = reader.directoryOffset - resourceStartOffset
|
||||
}
|
||||
var resourceSource io.Reader = io.NewSectionReader(reader.source, int64(resourceStartOffset), int64(resourceSize))
|
||||
if compressed {
|
||||
resourceSize = entry.unpackedLength()
|
||||
resourceSource = compression.NewDecompressor(resourceSource)
|
||||
}
|
||||
return io.LimitReader(resourceSource, int64(resourceSize)), nil
|
||||
}
|
||||
|
||||
return &readerResource{
|
||||
Properties: resource.Properties{
|
||||
Compound: false,
|
||||
ContentType: contentType,
|
||||
Compressed: compressed,
|
||||
},
|
||||
blockReader: blockReader{blockCount: 1, blockFunc: blockFunc}}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user