178 lines
5.7 KiB
Go
178 lines
5.7 KiB
Go
package lgres
|
|
|
|
import (
|
|
"io"
|
|
"math"
|
|
|
|
"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"
|
|
)
|
|
|
|
// Writer provides methods to write a new resource file from scratch.
|
|
// Resources have to be created sequentially. The writer does not support
|
|
// concurrent creation and modification of resources.
|
|
type Writer struct {
|
|
encoder *serial.PositioningEncoder
|
|
|
|
firstResourceOffset uint32
|
|
currentResourceStartOffset uint32
|
|
currentResource resourceWriter
|
|
|
|
directory []*resourceDirectoryEntry
|
|
}
|
|
|
|
// ErrTargetNil is returned if the target cannot be used.
|
|
const ErrTargetNil ss1.StringError = "target is nil"
|
|
|
|
// NewWriter returns a new Writer instance prepared to add resources.
|
|
// To finalize the created file, call Finish().
|
|
//
|
|
// This function will write initial information to the target and will return
|
|
// an error if the writer did. In such a case, the returned writer instance
|
|
// will produce invalid results and the state of the target is undefined.
|
|
func NewWriter(target io.WriteSeeker) (*Writer, error) {
|
|
if target == nil {
|
|
return nil, ErrTargetNil
|
|
}
|
|
|
|
encoder := serial.NewPositioningEncoder(target)
|
|
writer := &Writer{encoder: encoder}
|
|
writer.writeHeader()
|
|
writer.firstResourceOffset = writer.encoder.CurPos()
|
|
|
|
return writer, writer.encoder.FirstError()
|
|
}
|
|
|
|
// ErrWriterFinished is returned if the writer was already finished.
|
|
const ErrWriterFinished ss1.StringError = "writer is finished"
|
|
|
|
// CreateResource adds a new single-block resource to the current resource file.
|
|
// This resource is closed by creating another resource, or by finishing the writer.
|
|
func (writer *Writer) CreateResource(id resource.ID, contentType resource.ContentType,
|
|
compressed bool) (*BlockWriter, error) {
|
|
if writer.encoder == nil {
|
|
return nil, ErrWriterFinished
|
|
}
|
|
|
|
writer.finishLastResource()
|
|
if writer.encoder.FirstError() != nil {
|
|
return nil, writer.encoder.FirstError()
|
|
}
|
|
|
|
var targetWriter io.Writer = serial.NewEncoder(writer.encoder)
|
|
targetFinisher := func() {}
|
|
resourceType := byte(0x00)
|
|
if compressed {
|
|
compressor := compression.NewCompressor(targetWriter)
|
|
resourceType |= format.ResourceTypeFlagCompressed
|
|
targetWriter = compressor
|
|
targetFinisher = func() { compressor.Close() } // nolint: errcheck
|
|
}
|
|
blockWriter := &BlockWriter{target: targetWriter, finisher: targetFinisher}
|
|
writer.addNewResource(id, contentType, resourceType, blockWriter)
|
|
|
|
return blockWriter, nil
|
|
}
|
|
|
|
// CreateCompoundResource adds a new compound resource to the current resource file.
|
|
// This resource is closed by creating another resource, or by finishing the writer.
|
|
func (writer *Writer) CreateCompoundResource(id resource.ID, contentType resource.ContentType,
|
|
compressed bool) (*CompoundResourceWriter, error) {
|
|
if writer.encoder == nil {
|
|
return nil, ErrWriterFinished
|
|
}
|
|
|
|
writer.finishLastResource()
|
|
if writer.encoder.FirstError() != nil {
|
|
return nil, writer.encoder.FirstError()
|
|
}
|
|
|
|
resourceType := format.ResourceTypeFlagCompound
|
|
if compressed {
|
|
resourceType |= format.ResourceTypeFlagCompressed
|
|
}
|
|
resourceWriter := &CompoundResourceWriter{
|
|
target: serial.NewPositioningEncoder(writer.encoder),
|
|
compressed: compressed,
|
|
dataPaddingSize: writer.dataPaddingSizeForCompoundResource(id)}
|
|
writer.addNewResource(id, contentType, resourceType, resourceWriter)
|
|
|
|
return resourceWriter, nil
|
|
}
|
|
|
|
// Finish finalizes the resource file. After calling this function, the
|
|
// writer becomes unusable.
|
|
func (writer *Writer) Finish() (err error) {
|
|
if writer.encoder == nil {
|
|
return ErrWriterFinished
|
|
}
|
|
|
|
writer.finishLastResource()
|
|
|
|
directoryOffset := writer.encoder.CurPos()
|
|
writer.encoder.SetCurPos(format.ResourceDirectoryFileOffsetPos)
|
|
writer.encoder.Code(directoryOffset)
|
|
writer.encoder.SetCurPos(directoryOffset)
|
|
writer.encoder.Code(uint16(len(writer.directory)))
|
|
writer.encoder.Code(writer.firstResourceOffset)
|
|
for _, entry := range writer.directory {
|
|
writer.encoder.Code(entry)
|
|
}
|
|
|
|
err = writer.encoder.FirstError()
|
|
writer.encoder = nil
|
|
|
|
return
|
|
}
|
|
|
|
func (writer *Writer) writeHeader() {
|
|
header := make([]byte, format.ResourceDirectoryFileOffsetPos)
|
|
for index, r := range format.HeaderString {
|
|
header[index] = byte(r)
|
|
}
|
|
header[len(format.HeaderString)] = format.CommentTerminator
|
|
writer.encoder.Code(header)
|
|
writer.encoder.Code(uint32(math.MaxUint32))
|
|
}
|
|
|
|
func (writer *Writer) addNewResource(id resource.ID, contentType resource.ContentType, resourceType byte, newResource resourceWriter) {
|
|
entry := &resourceDirectoryEntry{ID: id.Value()}
|
|
entry.setContentType(byte(contentType))
|
|
entry.setResourceType(resourceType)
|
|
writer.directory = append(writer.directory, entry)
|
|
writer.currentResource = newResource
|
|
writer.currentResourceStartOffset = writer.encoder.CurPos()
|
|
}
|
|
|
|
func (writer *Writer) finishLastResource() {
|
|
if writer.currentResource != nil {
|
|
currentEntry := writer.directory[len(writer.directory)-1]
|
|
currentEntry.setUnpackedLength(writer.currentResource.finish())
|
|
currentEntry.setPackedLength(writer.encoder.CurPos() - writer.currentResourceStartOffset)
|
|
|
|
writer.currentResourceStartOffset = 0
|
|
writer.currentResource = nil
|
|
}
|
|
writer.alignToBoundary()
|
|
}
|
|
|
|
func (writer *Writer) alignToBoundary() {
|
|
extraBytes := writer.encoder.CurPos() % format.BoundarySize
|
|
if extraBytes > 0 {
|
|
padding := make([]byte, format.BoundarySize-extraBytes)
|
|
writer.encoder.Code(padding)
|
|
}
|
|
}
|
|
|
|
func (writer *Writer) dataPaddingSizeForCompoundResource(id resource.ID) (padding int) {
|
|
// Some directories have a 2byte padding before the actual data
|
|
idValue := id.Value()
|
|
if (idValue >= 0x08FC) && (idValue <= 0x094B) { // all resources in obj3d.res
|
|
padding = 2
|
|
}
|
|
return
|
|
}
|