Skip to content

Commit

Permalink
Duplicated from github.com/galaco/lambda-core
Browse files Browse the repository at this point in the history
  • Loading branch information
Galaco committed Sep 13, 2019
1 parent 3c63846 commit 6fbc2f0
Show file tree
Hide file tree
Showing 10 changed files with 382 additions and 2 deletions.
25 changes: 23 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,23 @@
# filesystem
Source engine filesystem manager
[![GoDoc](https://godoc.org/github.com/galaco/lambda-core?status.svg)](https://godoc.org/github.com/galaco/lambda-core)
[![Go report card](https://goreportcard.com/badge/github.com/galaco/lambda-core)](hhttps://goreportcard.com/report/github.com/galaco/lambda-core)
[![GolangCI](https://golangci.com/badges/github.com/galaco/lambda-core.svg)](https://golangci.com/r/github.com/golang-source-engine)
[![codecov](https://codecov.io/gh/golang-source-engine/branch/master/graph/badge.svg)](https://codecov.io/gh/golang-source-engine)
[![CircleCI](https://circleci.com/gh/golang-source-engine.svg?style=svg)](https://circleci.com/gh/golang-source-engine)

# Filesystem

> A filesystem utility for reading Source engine game structures.
Source Engine is a little annoying in that there are potentially unlimited possible
locations that engine resources can be located. Filesystem provides a way to register
and organise any potential resource path or filesystem, while preserving filesystem type
search priority.

A filesystem can either be manually defined, or created from a GameInfo.txt-derived KeyValues.

### Features
* Supports local directories
* Supports VPK's
* Supports BSP Pakfile
* Respects Source Engines search priority (pakfile->local directory->vpk)
* A ready to use Filesystem can be constructed from GameInfo.txt definitions
50 changes: 50 additions & 0 deletions errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package filesystem

import (
"fmt"
"strings"
)

// FileNotFoundError
type FileNotFoundError struct {
fileName string
}

// Error
func (err FileNotFoundError) Error() string {
return fmt.Sprintf("%s not found in filesystem", err.fileName)
}

// NewFileNotFoundError
func NewFileNotFoundError(filename string) *FileNotFoundError {
return &FileNotFoundError{
fileName: filename,
}
}

// InvalidResourcePathCollectionError
type InvalidResourcePathCollectionError struct {
paths []string
}

// Error will return a list of paths that could not be added.
// This list is a pipe-separated(|) string
func (err InvalidResourcePathCollectionError) Error() string {
msg := ""
for _,p := range err.paths {
msg += p + "|"
}
return strings.Trim(msg, "|")
}

// AddPath adds a new path to this error colleciton
func (err InvalidResourcePathCollectionError) AddPath(path string) {
err.paths = append(err.paths, path)
}

// NewInvalidResourcePathCollectionError
func NewInvalidResourcePathCollectionError() *InvalidResourcePathCollectionError {
return &InvalidResourcePathCollectionError{
paths: make([]string, 0),
}
}
128 changes: 128 additions & 0 deletions filesystem.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package filesystem

import (
"bytes"
"github.com/galaco/bsp/lumps"
"github.com/galaco/vpk2"
"io"
"io/ioutil"
"os"
"strings"
)

// FileSystem
type FileSystem struct {
gameVPKs map[string]vpk.VPK
localDirectories []string
pakFile *lumps.Pakfile
}

// NewFileSystem returns a new filesystem
func NewFileSystem() *FileSystem {
return &FileSystem{
gameVPKs: map[string]vpk.VPK{},
localDirectories: make([]string, 0),
pakFile: nil,
}
}

// PakFile returns loaded pakfile
// There can only be 1 registered pakfile at once.
func (fs *FileSystem) PakFile() *lumps.Pakfile {
return fs.pakFile
}

// RegisterVpk registers a vpk package as a valid
// asset directory
func (fs *FileSystem) RegisterVpk(path string, vpkFile *vpk.VPK) {
fs.gameVPKs[path] = *vpkFile
}

func (fs *FileSystem) UnregisterVpk(path string) {
for key := range fs.gameVPKs {
if key == path {
delete(fs.gameVPKs, key)
}
}
}

// RegisterLocalDirectory register a filesystem path as a valid
// asset directory
func (fs *FileSystem) RegisterLocalDirectory(directory string) {
fs.localDirectories = append(fs.localDirectories, directory)
}

func (fs *FileSystem) UnregisterLocalDirectory(directory string) {
for idx, dir := range fs.localDirectories {
if dir == directory {
if len(fs.localDirectories) == 1 {
fs.localDirectories = make([]string, 0)
return
}
fs.localDirectories = append(fs.localDirectories[:idx], fs.localDirectories[idx+1:]...)
}
}
}

// RegisterPakFile Set a pakfile to be used as an asset directory.
// This would normally be called during each map load
func (fs *FileSystem) RegisterPakFile(pakFile *lumps.Pakfile) {
fs.pakFile = pakFile
}

// UnregisterPakFile removes the current pakfile from
// available search locations
func (fs *FileSystem) UnregisterPakFile() {
fs.pakFile = nil
}

// EnumerateResourcePaths returns all registered resource paths.
// PakFile is excluded.
func (fs *FileSystem) EnumerateResourcePaths() []string {
list := make([]string, 0)

for idx := range fs.gameVPKs {
list = append(list, string(idx))
}

list = append(list, fs.localDirectories...)

return list
}

// GetFile attempts to get stream for filename.
// Search order is Pak->FileSystem->VPK
func (fs *FileSystem) GetFile(filename string) (io.Reader, error) {
// sanitise file
searchPath := NormalisePath(strings.ToLower(filename))

// try to read from pakfile first
if fs.pakFile != nil {
f, err := fs.pakFile.GetFile(searchPath)
if err == nil && f != nil && len(f) != 0 {
return bytes.NewReader(f), nil
}
}

// Fallback to local filesystem
for _, dir := range fs.localDirectories {
if _, err := os.Stat(dir + "\\" + searchPath); os.IsNotExist(err) {
continue
}
file, err := ioutil.ReadFile(dir + searchPath)
if err != nil {
return nil, err
}
return bytes.NewBuffer(file), nil
}

// Fall back to game vpk
for _, vfs := range fs.gameVPKs {
entry := vfs.Entry(searchPath)
if entry != nil {
return entry.Open()
}
}

return nil, NewFileNotFoundError(filename)
}
56 changes: 56 additions & 0 deletions filesystem_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package filesystem

import "testing"

func TestGetFile(t *testing.T) {
t.Skip()
}

func TestRegisterPakfile(t *testing.T) {
t.Skip()
}

func TestRegisterVpk(t *testing.T) {
t.Skip()
}

func TestUnregisterVpk(t *testing.T) {
t.Skip()
}

func TestRegisterLocalDirectory(t *testing.T) {
fs := NewFileSystem()
dir := "foo/bar/baz"
fs.RegisterLocalDirectory(dir)
found := false
for _, path := range fs.localDirectories {
if path == dir {
found = true
break
}
}
if found == false {
t.Error("local filepath was not found in registered paths")
}
}

func TestUnregisterLocalDirectory(t *testing.T) {
fs := NewFileSystem()
dir := "foo/bar/baz"
fs.RegisterLocalDirectory(dir)
fs.UnregisterLocalDirectory(dir)
found := false
for _, path := range fs.localDirectories {
if path == dir {
found = true
break
}
}
if found == true {
t.Error("local filepath was not found in registered paths")
}
}

func TestUnregisterPakfile(t *testing.T) {
t.Skip()
}
9 changes: 9 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module github.com/golang-source-engine/filesystem

go 1.13

require (
github.com/galaco/KeyValues v1.3.1
github.com/galaco/bsp v0.2.1
github.com/galaco/vpk2 v0.0.0-20181012095330-21e4d1f6c888
)
9 changes: 9 additions & 0 deletions path.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package filesystem

import "strings"

// NormalisePath ensures that the same filepath format is used for paths,
// regardless of platform.
func NormalisePath(filePath string) string {
return strings.Replace(filePath, "\\", "/", -1)
}
12 changes: 12 additions & 0 deletions path_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package filesystem

import "testing"

func TestNormalisePath(t *testing.T) {
path := "foo\\bar\\baz"
expected := "foo/bar/baz"
actual := NormalisePath(path)
if expected != actual {
t.Errorf("incorrect path normalised. Expected %s, but received: %s", expected, actual)
}
}
77 changes: 77 additions & 0 deletions register.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package filesystem

import (
"github.com/galaco/KeyValues"
"path/filepath"
"regexp"
"strings"
)

// CreateFilesystemFromGameInfoDefinitions Reads game resource data paths
// from gameinfo.txt
// All games should ship with a gameinfo.txt, but it isn't actually mandatory.
// GameInfo definitions are quite unreliable, there are often bad entries;
// allowInvalidLocations will skip over bad paths, and an error collection
// will be returned will all paths that are invalid.
func CreateFilesystemFromGameInfoDefinitions(basePath string, gameInfo *keyvalues.KeyValue, allowInvalidLocations bool) (*FileSystem, error) {
fs := NewFileSystem()
gameInfoNode, _ := gameInfo.Find("GameInfo")
fsNode, _ := gameInfoNode.Find("FileSystem")

searchPathsNode, _ := fsNode.Find("SearchPaths")
searchPaths, _ := searchPathsNode.Children()
basePath, _ = filepath.Abs(basePath)
basePath = strings.Replace(basePath, "\\", "/", -1)

badPathErrorCollection := NewInvalidResourcePathCollectionError()

for _, searchPath := range searchPaths {
kv := searchPath
path, _ := kv.AsString()
path = strings.Trim(path, " ")

// Current directory
gameInfoPathRegex := regexp.MustCompile(`(?i)\|gameinfo_path\|`)
if gameInfoPathRegex.MatchString(path) {
path = gameInfoPathRegex.ReplaceAllString(path, basePath+"/")
}

// Executable directory
allSourceEnginePathsRegex := regexp.MustCompile(`(?i)\|all_source_engine_paths\|`)
if allSourceEnginePathsRegex.MatchString(path) {
path = allSourceEnginePathsRegex.ReplaceAllString(path, basePath+"/../")
}
if strings.Contains(strings.ToLower(kv.Key()), "mod") && !strings.HasPrefix(path, basePath) {
path = basePath + "/../" + path
}

// Strip vpk extension, then load it
path = strings.Trim(strings.Trim(path, " "), "\"")
if strings.HasSuffix(path, ".vpk") {
path = strings.Replace(path, ".vpk", "", 1)
vpkHandle, err := openVPK(path)
if err != nil {
if !allowInvalidLocations {
return nil, err
}
badPathErrorCollection.AddPath(path)
continue
}
fs.RegisterVpk(path, vpkHandle)
} else {
// wildcard suffixes not useful
if strings.HasSuffix(path, "/*") {
path = strings.Replace(path, "/*", "", -1)
}
fs.RegisterLocalDirectory(path)
}
}

// A filesystem can be valid, even if some GameInfo defined locations
// were not.
if allowInvalidLocations && len(badPathErrorCollection.paths) > 0 {
return fs, badPathErrorCollection
}

return fs, nil
}
7 changes: 7 additions & 0 deletions register_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package filesystem

import "testing"

func TestRegisterGameResourcePaths(t *testing.T) {
t.Skip()
}
11 changes: 11 additions & 0 deletions vpk.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package filesystem

import (
"github.com/galaco/vpk2"
)

// openVPK Basic wrapper around vpk library.
// Just opens a multi-part vpk (ver 2 only)
func openVPK(filepath string) (*vpk.VPK, error) {
return vpk.Open(vpk.MultiVPK(filepath))
}

0 comments on commit 6fbc2f0

Please sign in to comment.