Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main'
Browse files Browse the repository at this point in the history
  • Loading branch information
halamix2 committed Jan 19, 2023
2 parents 073d8e6 + 55fdad4 commit 80b3a0e
Show file tree
Hide file tree
Showing 18 changed files with 798 additions and 244 deletions.
2 changes: 0 additions & 2 deletions 010_editor_templates/dir.bt
Original file line number Diff line number Diff line change
Expand Up @@ -58,5 +58,3 @@ string readFile(File &f) {
string name;
return f.filename;
};


7 changes: 6 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,15 @@ COVER_FILE=cover.out

all: build test vet cover

build_all: build_linux_64 build_windows_32

build:
go build -ldflags "-s -w" -o ${OUTPUT_DIR} ./cmd/...

build_windows:
build_linux_64:
GOOS=linux GOARCH=amd64 go build -ldflags "-s -w" -o ${OUTPUT_DIR} ./cmd/...

build_windows_32:
GOOS=windows GOARCH=386 go build -ldflags "-s -w" -o ${OUTPUT_DIR} ./cmd/...
test:
go test ./cmd/... ./pkg/...
Expand Down
101 changes: 101 additions & 0 deletions cmd/dir_pack/dir_pack.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
Dir_pack packs directories to .dir/.wad files
*/
package main

import (
"fmt"
"io/fs"
"os"
"path/filepath"
"strings"

"github.com/halamix2/stunt_gp_tools/pkg/dir"
"github.com/spf13/pflag"
)

// flags
var (
outputName string
// verbose bool
)

func parseFlags() {
pflag.StringVarP(&outputName, "output", "o", "", "name of the output file")
// verbose
pflag.Parse()
}

func usage() {
fmt.Println("Packs dir archive used by Stunt GP and Worms Armageddon.")
fmt.Println("Usage:\ndir_pack input_dir [input_dirs]")
fmt.Println("Flags:")
pflag.PrintDefaults()
}

func main() {
parseFlags()
args := pflag.Args()
if len(args) < 1 {
usage()
os.Exit(1)
}

if outputName != "" && len(args) > 1 {
fmt.Println("Output name can only be used with one input directory")
usage()
os.Exit(1)
}

for _, inputName := range args {
if outputName == "" {
// I know, Worms uses .dir extension, but this is primarily Stunt GP program after all
outputName = strings.TrimSuffix(inputName, filepath.Ext(inputName)) + ".wad"
}

dir := dir.Dir{}

walkFunc := getWalkFunc(&dir)
err := filepath.Walk(inputName, walkFunc)
if err != nil {
fmt.Fprintf(os.Stderr, "Couldn't get list of files: %s\n", outputName)
os.Exit(1)
}
fmt.Printf("packing %d files\n", len(dir))

outputFile, err := os.Create(outputName)
if err != nil {
fmt.Fprintf(os.Stderr, "Couldn't create output image file %s!\n", outputName)
os.Exit(1)
}

err = dir.ToWriter(outputFile)
if err != nil {
fmt.Fprintf(os.Stderr, "Couldn't save dir file %s: %s\n", outputName, err)
os.Exit(1)
}

err = outputFile.Close()
if err != nil {
fmt.Fprintf(os.Stderr, "Couldn't close output file %s: %s\n", outputName, err)
os.Exit(1)
}
}
}

func getWalkFunc(dir *dir.Dir) filepath.WalkFunc {
return func(path string, info fs.FileInfo, err error) error {
if !info.IsDir() {
// file
data, err := os.ReadFile(path)
if err != nil {
return err
}
pathNormalized := strings.ReplaceAll(path, "/", "\\")
split := strings.Split(pathNormalized, "\\")
pathNormalized = strings.Join(split[1:], "\\")
(*dir)[pathNormalized] = data
}
return nil
}
}
90 changes: 90 additions & 0 deletions cmd/dir_unpack/dir_unpack.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
Dir_unpack unpacks .dir/.wad files to directories
*/
package main

import (
"fmt"
"os"
"path"
"path/filepath"
"strings"

"github.com/halamix2/stunt_gp_tools/pkg/dir"
"github.com/spf13/pflag"
)

// flags
var (
outputDirectory string
)

func parseFlags() {
pflag.StringVarP(&outputDirectory, "output", "o", "", "name of the output directory")
pflag.Parse()
}

func usage() {
fmt.Println("Unpacks dir archive used by Stunt GP and Worms Armageddon.")
fmt.Println("Usage:\ndir_unpack input_file [input_files]")
fmt.Println("Flags:")
pflag.PrintDefaults()
}

func main() {
parseFlags()
args := pflag.Args()
if len(args) < 1 {
usage()
os.Exit(1)
}

if outputDirectory != "" && len(args) > 1 {
fmt.Println("Output name can only be used with one input directory")
usage()
os.Exit(1)
}

for _, inputName := range args {
if outputDirectory == "" {
// I know, Worms uses .dir extension, but this is primarily Stunt GP program after all
outputDirectory = strings.TrimSuffix(inputName, filepath.Ext(inputName))
}

dirFile, err := dir.FromDir(inputName)
if err != nil {
fmt.Fprintf(os.Stderr, "Couldn't read Dir file: %s\n", err)
os.Exit(1)
}

err = os.MkdirAll(outputDirectory, 0750)
if err != nil {
fmt.Fprintf(os.Stderr, "Couldn't create %s path: %s\n", outputDirectory, err)
os.Exit(1)
}

err = saveFiles(dirFile)
if err != nil {
fmt.Fprintf(os.Stderr, "Couldn't save files: %s\n", err)
os.Exit(1)
}
}
}

func saveFiles(dirFile dir.Dir) error {
for archFilename, archData := range dirFile {
archFilename = strings.ReplaceAll(archFilename, "\\", "/")
outPath := filepath.Join(outputDirectory, archFilename)

err := os.MkdirAll(path.Dir(outPath), 0750)
if err != nil {
return fmt.Errorf("couldn't create %s path: %s", outPath, err)
}

err = os.WriteFile(outPath, archData, 0666)
if err != nil {
return fmt.Errorf("couldn't save %s file: %s", outPath, err)
}
}
return nil
}
6 changes: 2 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@ go 1.19
require (
github.com/gojuno/go.morton v0.0.0-20180202102823-94709bd871ce
github.com/spf13/cobra v1.6.1
github.com/spf13/pflag v1.0.5
)

require (
github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
)
require github.com/inconshreveable/mousetrap v1.0.1 // indirect
50 changes: 50 additions & 0 deletions pkg/dir/common.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Package dir implements functions used to manipulate Worms Armageddon / Stunt GP .dir / .wad files.
package dir

const testDir = "test_data/"

// Dir is a base representation of a Dir archive
type Dir map[string][]byte

// HashTable stores whole hash table
type HashTable struct {
Header [4]byte
Entries [1024]uint32
}

// Header contains all data at the beginning of a file
type Header struct {
Magic [4]byte
Size uint32
DirectoryAddress uint32
}

// FileMetadata contains all metadata stored in the file list
type FileMetadata struct {
NextHash uint32
DataOffset uint32
DataSize uint32
Filename string
}

// FileMetadataSmall contains metadata stored in the file list without the filename
type FileMetadataSmall struct {
NextHash uint32
DataOffset uint32
DataSize uint32
}

// GetHash generates hash used by the DIR archives. Filename should be provided in ASCII encoding and without trailing \0
func GetHash(filename string) uint32 {
var hash uint32

const hashBits uint32 = 10
const hashSize uint32 = 1 << hashBits

for _, char := range filename {
hash = (uint32(hash<<1) % hashSize) | uint32(hash>>(hashBits-1)&1)
hash = uint32(hash+uint32(char)) % hashSize
}

return hash
}
45 changes: 45 additions & 0 deletions pkg/dir/common_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package dir

import (
"testing"
)

func TestHashingFunction(t *testing.T) {
// names should be provided without trailing \0, but the tests also check for that
testCases := []struct {
path string
expectedHash uint32
}{
{
path: "",
expectedHash: 0,
},
{
path: "a",
expectedHash: 97,
},
{
path: "test",
expectedHash: 655,
},
{
path: "test\x00",
expectedHash: 287,
},
{
path: "graphics24\\cars\\car15\\b15grid5.pc",
expectedHash: 341,
},
{
path: "replays\\track19.rpl",
expectedHash: 738,
},
}

for _, testCase := range testCases {
hash := GetHash(testCase.path)
if hash != testCase.expectedHash {
t.Fatalf("HashingFunction: path: %s, expected hash: %d, got %d", testCase.path, testCase.expectedHash, hash)
}
}
}
Loading

0 comments on commit 80b3a0e

Please sign in to comment.