diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3b6dea0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +# Local Go build cache +.cache/ + +# Built tool output +/tools/nwn-tool + +# OS/editor noise +.DS_Store +Thumbs.db diff --git a/internal/erf/erf.go b/internal/erf/erf.go index 07dda40..34cebff 100644 --- a/internal/erf/erf.go +++ b/internal/erf/erf.go @@ -5,6 +5,7 @@ import ( "encoding/binary" "fmt" "io" + "os" "sort" "strings" ) @@ -24,9 +25,11 @@ type Archive struct { } type Resource struct { - Name string - Type uint16 - Data []byte + Name string + Type uint16 + Data []byte + SourcePath string + Size int64 } type header struct { @@ -139,7 +142,7 @@ func New(fileType string, resources []Resource) Archive { } func ArchiveSize(resources []Resource) int64 { - return int64(headerSize + len(resources)*24 + len(resources)*8 + totalResourceBytes(resources)) + return int64(headerSize+len(resources)*24+len(resources)*8) + totalResourceBytes(resources) } func Write(w io.Writer, archive Archive) error { @@ -154,28 +157,30 @@ func Write(w io.Writer, archive Archive) error { entries := make([]resourceEntry, 0, len(archive.Resources)) dataOffset := uint32(headerSize + len(archive.Resources)*24 + len(archive.Resources)*8) - data := bytes.Buffer{} - for index, resource := range archive.Resources { if len(resource.Name) > 16 { return fmt.Errorf("resource %q exceeds 16-byte resref limit", resource.Name) } + size, err := payloadSize(resource) + if err != nil { + return err + } + if size > int64(^uint32(0)) { + return fmt.Errorf("resource %q exceeds 4 GiB resource size limit", resource.Name) + } entry := resourceEntry{ - Offset: dataOffset + uint32(data.Len()), - Size: uint32(len(resource.Data)), + Offset: dataOffset, + Size: uint32(size), } entries = append(entries, entry) + dataOffset += uint32(size) var key keyEntry copy(key.ResRef[:], []byte(resource.Name)) key.ResourceID = uint32(index) key.ResourceType = resource.Type keys = append(keys, key) - - if _, err := data.Write(resource.Data); err != nil { - return err - } } hdr := header{ @@ -192,22 +197,21 @@ func Write(w io.Writer, archive Archive) error { copy(hdr.FileType[:], []byte(padFour(archive.FileType))) copy(hdr.Version[:], []byte(padFour(archive.Version))) - var buf bytes.Buffer - if err := binary.Write(&buf, binary.LittleEndian, hdr); err != nil { + if err := binary.Write(w, binary.LittleEndian, hdr); err != nil { return err } - if err := binary.Write(&buf, binary.LittleEndian, keys); err != nil { + if err := binary.Write(w, binary.LittleEndian, keys); err != nil { return err } - if err := binary.Write(&buf, binary.LittleEndian, entries); err != nil { + if err := binary.Write(w, binary.LittleEndian, entries); err != nil { return err } - if _, err := buf.Write(data.Bytes()); err != nil { - return err + for _, resource := range archive.Resources { + if err := writeResourceData(w, resource); err != nil { + return err + } } - - _, err := w.Write(buf.Bytes()) - return err + return nil } func Read(r io.Reader) (Archive, error) { @@ -260,6 +264,7 @@ func Read(r io.Reader) (Archive, error) { Name: resref, Type: key.ResourceType, Data: payload, + Size: int64(entry.Size), }) } @@ -288,10 +293,51 @@ func padFour(value string) string { return value + strings.Repeat(" ", 4-len(value)) } -func totalResourceBytes(resources []Resource) int { - total := 0 +func totalResourceBytes(resources []Resource) int64 { + var total int64 for _, resource := range resources { - total += len(resource.Data) + switch { + case resource.Size > 0: + total += resource.Size + default: + total += int64(len(resource.Data)) + } } return total } + +func payloadSize(resource Resource) (int64, error) { + if resource.Size > 0 { + return resource.Size, nil + } + if resource.SourcePath != "" && len(resource.Data) == 0 { + info, err := os.Stat(resource.SourcePath) + if err != nil { + return 0, fmt.Errorf("stat resource %q: %w", resource.SourcePath, err) + } + return info.Size(), nil + } + return int64(len(resource.Data)), nil +} + +func writeResourceData(w io.Writer, resource Resource) error { + if len(resource.Data) > 0 || resource.SourcePath == "" { + _, err := w.Write(resource.Data) + return err + } + + file, err := os.Open(resource.SourcePath) + if err != nil { + return fmt.Errorf("open resource %q: %w", resource.SourcePath, err) + } + defer file.Close() + + written, err := io.Copy(w, file) + if err != nil { + return fmt.Errorf("copy resource %q: %w", resource.SourcePath, err) + } + if resource.Size > 0 && written != resource.Size { + return fmt.Errorf("copy resource %q: expected %d bytes, wrote %d", resource.SourcePath, resource.Size, written) + } + return nil +} diff --git a/internal/pipeline/build.go b/internal/pipeline/build.go index 1a194ac..d9246ff 100644 --- a/internal/pipeline/build.go +++ b/internal/pipeline/build.go @@ -45,12 +45,11 @@ type assetResource struct { } type hakChunk struct { - Config project.HAKConfig - Index int - Name string - Assets []assetResource - Size int64 - Resources []erf.Resource + Config project.HAKConfig + Index int + Name string + Assets []assetResource + Size int64 } func Build(p *project.Project) (BuildResult, error) { @@ -132,7 +131,7 @@ func BuildHAKs(p *project.Project) (BuildResult, error) { if err != nil { return BuildResult{}, fmt.Errorf("create hak archive: %w", err) } - if err := erf.Write(hakOutput, erf.New("HAK ", chunk.Resources)); err != nil { + if err := erf.Write(hakOutput, erf.New("HAK ", resourceSlice(chunk.Assets))); err != nil { hakOutput.Close() return BuildResult{}, fmt.Errorf("write hak archive: %w", err) } @@ -198,7 +197,7 @@ func collectAssetResources(p *project.Project) ([]assetResource, error) { for _, rel := range p.Inventory.AssetFiles { abs := filepath.Join(p.AssetsDir(), filepath.FromSlash(rel)) - resource, err := rawResource(abs) + resource, err := pathResource(abs) if err != nil { return nil, err } @@ -276,6 +275,25 @@ func resourceFromJSON(path string, moduleHakOrder []string) (erf.Resource, error }, nil } +func pathResource(path string) (erf.Resource, error) { + extension := strings.TrimPrefix(strings.ToLower(filepath.Ext(path)), ".") + resourceType, ok := erf.ResourceTypeForExtension(extension) + if !ok { + return erf.Resource{}, fmt.Errorf("unsupported resource extension %q", filepath.Ext(path)) + } + name := strings.TrimSuffix(filepath.Base(path), filepath.Ext(path)) + info, err := os.Stat(path) + if err != nil { + return erf.Resource{}, fmt.Errorf("stat %s: %w", path, err) + } + return erf.Resource{ + Name: name, + Type: resourceType, + SourcePath: path, + Size: info.Size(), + }, nil +} + func rawResource(path string) (erf.Resource, error) { extension := strings.TrimPrefix(strings.ToLower(filepath.Ext(path)), ".") resourceType, ok := erf.ResourceTypeForExtension(extension) @@ -317,11 +335,10 @@ func planHAKChunks(p *project.Project, assets []assetResource) ([]hakChunk, erro Name: p.Config.Module.ResRef, Priority: 1, }, - Index: 1, - Name: p.Config.Module.ResRef, - Size: erf.ArchiveSize(resourceSlice(assets)), - Assets: assets, - Resources: resourceSlice(assets), + Index: 1, + Name: p.Config.Module.ResRef, + Size: erf.ArchiveSize(resourceSlice(assets)), + Assets: assets, } return []hakChunk{chunk}, nil } @@ -373,12 +390,11 @@ func splitHAKGroup(cfg project.HAKConfig, assets []assetResource) ([]hakChunk, e maxBytes := cfg.MaxBytes if maxBytes == 0 { chunk := hakChunk{ - Config: cfg, - Index: 1, - Name: cfg.Name, - Assets: assets, - Resources: resourceSlice(assets), - Size: erf.ArchiveSize(resourceSlice(assets)), + Config: cfg, + Index: 1, + Name: cfg.Name, + Assets: assets, + Size: erf.ArchiveSize(resourceSlice(assets)), } if err := ensureUniqueChunkResources(chunk); err != nil { return nil, err @@ -398,14 +414,12 @@ func splitHAKGroup(cfg project.HAKConfig, assets []assetResource) ([]hakChunk, e return nil, fmt.Errorf("asset %s alone exceeds max_bytes for hak %s", asset.Rel, cfg.Name) } current.Assets = candidateAssets - current.Resources = candidateResources current.Size = candidateSize continue } if candidateSize <= maxBytes { current.Assets = candidateAssets - current.Resources = candidateResources current.Size = candidateSize continue } @@ -421,11 +435,10 @@ func splitHAKGroup(cfg project.HAKConfig, assets []assetResource) ([]hakChunk, e chunks = append(chunks, current) current = hakChunk{ - Config: cfg, - Index: current.Index + 1, - Assets: []assetResource{asset}, - Resources: []erf.Resource{asset.Resource}, - Size: erf.ArchiveSize([]erf.Resource{asset.Resource}), + Config: cfg, + Index: current.Index + 1, + Assets: []assetResource{asset}, + Size: erf.ArchiveSize([]erf.Resource{asset.Resource}), } if current.Size > maxBytes { return nil, fmt.Errorf("asset %s alone exceeds max_bytes for hak %s", asset.Rel, cfg.Name) diff --git a/tools/nwn-tool b/tools/nwn-tool deleted file mode 100755 index 9b1cf93..0000000 Binary files a/tools/nwn-tool and /dev/null differ