package topdata import ( "fmt" "io" "os" "path/filepath" "slices" "strings" "gitea.westgate.pw/ShadowsOverWestgate/sow-tools/internal/erf" "gitea.westgate.pw/ShadowsOverWestgate/sow-tools/internal/project" ) const ( PackageHAKResRef = "sow_top" PackageHAKFileName = PackageHAKResRef + ".hak" PackageTLKFileName = defaultTLKName ) func BuildPackage(p *project.Project, progress func(string)) (PackageResult, error) { if progress == nil { progress = func(string) {} } nativeResult, err := BuildNative(p, progress) if err != nil { return PackageResult{}, err } return packageBuiltTopData(p, nativeResult, progress) } func packageBuiltTopData(p *project.Project, nativeResult BuildResult, progress func(string)) (PackageResult, error) { if progress == nil { progress = func(string) {} } buildDir := p.BuildDir() outputHAK := filepath.Join(buildDir, PackageHAKFileName) outputTLK := filepath.Join(buildDir, PackageTLKFileName) progress("Packaging compiled topdata resources into sow_top.hak...") resources, assetFiles, err := collectTopPackageResources(p, nativeResult.Output2DADir) if err != nil { return PackageResult{}, err } if err := writeHAK(outputHAK, resources); err != nil { return PackageResult{}, err } progress("Preparing compiled TLK release output...") sourceTLK, err := resolveCompiledTLKPath(nativeResult.OutputTLKDir) if err != nil { return PackageResult{}, err } if err := copyFile(outputTLK, sourceTLK); err != nil { return PackageResult{}, err } return PackageResult{ Mode: nativeResult.Mode, Output2DADir: nativeResult.Output2DADir, OutputTLKDir: nativeResult.OutputTLKDir, OutputHAKPath: outputHAK, OutputTLKPath: outputTLK, Files2DA: nativeResult.Files2DA, FilesTLK: nativeResult.FilesTLK, HAKResources: len(resources), AssetFiles: assetFiles, }, nil } func collectTopPackageResources(p *project.Project, compiled2DADir string) ([]erf.Resource, int, error) { resourceByKey := map[string]erf.Resource{} entries, err := os.ReadDir(compiled2DADir) if err != nil { return nil, 0, fmt.Errorf("read compiled 2da output dir: %w", err) } for _, entry := range entries { if entry.IsDir() || !strings.EqualFold(filepath.Ext(entry.Name()), ".2da") { continue } path := filepath.Join(compiled2DADir, entry.Name()) resource, err := topPackageResourceFromPath(path) if err != nil { return nil, 0, err } resourceByKey[topPackageResourceKey(resource)] = resource } assetFiles := 0 assetsDir := filepath.Join(p.TopDataSourceDir(), "assets") info, err := os.Stat(assetsDir) if err == nil && info.IsDir() { err = filepath.WalkDir(assetsDir, func(path string, d os.DirEntry, walkErr error) error { if walkErr != nil { return walkErr } if d.IsDir() { return nil } if strings.HasPrefix(filepath.Base(path), ".") { return nil } resource, err := topPackageResourceFromPath(path) if err != nil { return err } key := topPackageResourceKey(resource) if _, ok := resourceByKey[key]; ok { return fmt.Errorf("topdata asset %s collides with generated top package resource %s", path, key) } resourceByKey[key] = resource assetFiles++ return nil }) if err != nil { return nil, 0, err } } else if err != nil && !os.IsNotExist(err) { return nil, 0, fmt.Errorf("stat topdata assets dir: %w", err) } keys := make([]string, 0, len(resourceByKey)) for key := range resourceByKey { keys = append(keys, key) } slices.Sort(keys) resources := make([]erf.Resource, 0, len(keys)) for _, key := range keys { resources = append(resources, resourceByKey[key]) } return resources, assetFiles, nil } func topPackageResourceFromPath(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 top package resource extension %q", 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: strings.ToLower(strings.TrimSuffix(filepath.Base(path), filepath.Ext(path))), Type: resourceType, SourcePath: path, Size: info.Size(), }, nil } func topPackageResourceKey(resource erf.Resource) string { extension, ok := erf.ExtensionForResourceType(resource.Type) if !ok { return strings.ToLower(resource.Name) } return strings.ToLower(resource.Name) + "." + strings.TrimPrefix(strings.ToLower(extension), ".") } func writeHAK(path string, resources []erf.Resource) error { output, err := os.Create(path) if err != nil { return fmt.Errorf("create top package hak: %w", err) } defer output.Close() if err := erf.Write(output, erf.New("HAK ", resources)); err != nil { return fmt.Errorf("write top package hak: %w", err) } return nil } func resolveCompiledTLKPath(outputDir string) (string, error) { entries, err := os.ReadDir(outputDir) if err != nil { return "", fmt.Errorf("read compiled tlk output dir: %w", err) } var tlkFiles []string for _, entry := range entries { if entry.IsDir() || !strings.EqualFold(filepath.Ext(entry.Name()), ".tlk") { continue } tlkFiles = append(tlkFiles, filepath.Join(outputDir, entry.Name())) } if len(tlkFiles) != 1 { return "", fmt.Errorf("expected exactly 1 compiled tlk output, found %d", len(tlkFiles)) } return tlkFiles[0], nil } func copyFile(dst, src string) error { input, err := os.Open(src) if err != nil { return fmt.Errorf("open %s: %w", src, err) } defer input.Close() output, err := os.Create(dst) if err != nil { return fmt.Errorf("create %s: %w", dst, err) } defer output.Close() if _, err := io.Copy(output, input); err != nil { return fmt.Errorf("copy %s to %s: %w", src, dst, err) } return nil }