Adding modding folder with a hacked build.
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
package world
|
||||
|
||||
import "github.com/inkyblackness/hacked/ss1/resource"
|
||||
|
||||
// BlockPatch describes a raw delta-change of a resource block.
|
||||
// The change can be applied, and reverted.
|
||||
type BlockPatch struct {
|
||||
// ID identifier of the resource
|
||||
ID resource.ID
|
||||
// BlockIndex identifies the block for which the patch applies.
|
||||
BlockIndex int
|
||||
// BlockLength describes the expected length of the block.
|
||||
BlockLength int
|
||||
|
||||
// ForwardData contains the delta information to patch to a new version.
|
||||
ForwardData []byte
|
||||
// ReverseData contains the delta information to change a new version back to the original.
|
||||
ReverseData []byte
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package world
|
||||
|
||||
import "github.com/inkyblackness/hacked/ss1"
|
||||
|
||||
const (
|
||||
// StartingLevel identifies the level a new game is started in by default.
|
||||
StartingLevel = 1
|
||||
// StartingTileX identifies the default X position of the protagonist.
|
||||
StartingTileX = 30
|
||||
// StartingTileY identifies the default Y position of the protagonist.
|
||||
StartingTileY = 22
|
||||
|
||||
// MaxWorldTextures is the limit of how many textures the engine supports.
|
||||
// Note that this value is equal to that of the resource limits in package ids. It is actually based on them.
|
||||
MaxWorldTextures = 293
|
||||
)
|
||||
|
||||
const (
|
||||
// TexturePropertiesFilename specifies the lowercase name of the file containing texture properties.
|
||||
TexturePropertiesFilename = "textprop.dat"
|
||||
|
||||
// ObjectPropertiesFilename specifies the lowercase name of the file containing object properties.
|
||||
ObjectPropertiesFilename = "objprop.dat"
|
||||
)
|
||||
|
||||
const (
|
||||
errNoResourcesFound ss1.StringError = "resource unknown"
|
||||
errBlockLengthMismatch ss1.StringError = "block length mismatch"
|
||||
errIndexOutOfBounds ss1.StringError = "index is out of bounds"
|
||||
errEntryIsNil ss1.StringError = "entry is nil"
|
||||
)
|
||||
@@ -0,0 +1,8 @@
|
||||
package world
|
||||
|
||||
// IsConsideredCyberspaceByDefault returns true for those level identifier
|
||||
// that are hardcoded to be cyberspace levels.
|
||||
// For the vanilla engine, this is true for level 10 and any above 13.
|
||||
func IsConsideredCyberspaceByDefault(levelID int) bool {
|
||||
return (levelID == 10) || (levelID > 13)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package world_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/world"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDefaultCyberspaceLevels(t *testing.T) {
|
||||
tt := []bool{
|
||||
false, false, false, false,
|
||||
false, false, false, false,
|
||||
false, false, true, false,
|
||||
false, false, true, true,
|
||||
}
|
||||
|
||||
for levelID, expected := range tt {
|
||||
result := world.IsConsideredCyberspaceByDefault(levelID)
|
||||
assert.Equal(t, expected, result, "Cyberspace mismatch")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
package world
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/content/archive"
|
||||
"github.com/inkyblackness/hacked/ss1/content/object"
|
||||
"github.com/inkyblackness/hacked/ss1/content/texture"
|
||||
"github.com/inkyblackness/hacked/ss1/resource"
|
||||
"github.com/inkyblackness/hacked/ss1/resource/lgres"
|
||||
"github.com/inkyblackness/hacked/ss1/serial"
|
||||
"github.com/inkyblackness/hacked/ss1/world/ids"
|
||||
)
|
||||
|
||||
var fileAllowlist = ids.FilenameList{
|
||||
ids.Archive,
|
||||
ids.CybStrng,
|
||||
ids.CitALog,
|
||||
ids.CitBark,
|
||||
ids.CitMat,
|
||||
ids.DigiFX,
|
||||
ids.GamePal,
|
||||
ids.GameScr,
|
||||
ids.MfdArt,
|
||||
ids.Obj3D,
|
||||
ids.ObjArt,
|
||||
ids.ObjArt2,
|
||||
ids.ObjArt3,
|
||||
ids.SideArt,
|
||||
ids.Splash,
|
||||
ids.SvgaDeth,
|
||||
ids.SvgaEnd,
|
||||
ids.SvgaIntr,
|
||||
ids.Texture,
|
||||
ids.VidMail,
|
||||
}
|
||||
|
||||
type fileLoader struct {
|
||||
resultMutex sync.Mutex
|
||||
|
||||
allowZips bool
|
||||
|
||||
result FileLoadResult
|
||||
}
|
||||
|
||||
// FileLoadResult contains all the information of a LoadFiles attempt.
|
||||
type FileLoadResult struct {
|
||||
FailedFiles int
|
||||
Savegames map[FileLocation]resource.Viewer
|
||||
Resources map[FileLocation]resource.Viewer
|
||||
|
||||
ObjectProperties object.PropertiesTable
|
||||
TextureProperties texture.PropertiesList
|
||||
}
|
||||
|
||||
// LoadFiles attempts to load compatible files from the given set of filenames.
|
||||
func LoadFiles(allowZips bool, names []string) FileLoadResult {
|
||||
loader := fileLoader{
|
||||
allowZips: allowZips,
|
||||
result: FileLoadResult{
|
||||
Resources: make(map[FileLocation]resource.Viewer),
|
||||
Savegames: make(map[FileLocation]resource.Viewer),
|
||||
},
|
||||
}
|
||||
loader.loadAll(names)
|
||||
return loader.result
|
||||
}
|
||||
|
||||
func (loader *fileLoader) loadAll(names []string) {
|
||||
loader.loadList(names, len(names) == 1)
|
||||
}
|
||||
|
||||
func (loader *fileLoader) loadList(names []string, isOnlyRequestedFile bool) {
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for _, name := range names {
|
||||
wg.Add(1)
|
||||
go func(name string) {
|
||||
defer wg.Done()
|
||||
loader.load(name, isOnlyRequestedFile)
|
||||
}(name)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func (loader *fileLoader) load(name string, isOnlyRequestedFile bool) {
|
||||
fileInfo, err := os.Stat(name)
|
||||
if err != nil {
|
||||
loader.markFailedFile()
|
||||
return
|
||||
}
|
||||
file, err := os.Open(name)
|
||||
if err != nil {
|
||||
loader.markFailedFile()
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_ = file.Close() // nolint: gas
|
||||
}()
|
||||
|
||||
if fileInfo.IsDir() {
|
||||
if isOnlyRequestedFile {
|
||||
subNames, _ := file.Readdirnames(0)
|
||||
joinedSubNames := make([]string, len(subNames))
|
||||
for index, subName := range subNames {
|
||||
joinedSubNames[index] = filepath.Join(name, subName)
|
||||
}
|
||||
loader.loadList(joinedSubNames, false)
|
||||
}
|
||||
} else {
|
||||
tryDirect := true
|
||||
if loader.allowZips && isOnlyRequestedFile {
|
||||
tryDirect = false
|
||||
zipReader, err := zip.NewReader(file, fileInfo.Size())
|
||||
if err != nil {
|
||||
_, _ = file.Seek(0, io.SeekStart)
|
||||
tryDirect = true
|
||||
} else {
|
||||
loader.loadFileArchive(zipReader)
|
||||
}
|
||||
}
|
||||
|
||||
if tryDirect {
|
||||
loader.loadFile(name, isOnlyRequestedFile, file)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (loader *fileLoader) loadFileArchive(zipReader *zip.Reader) {
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for _, file := range zipReader.File {
|
||||
wg.Add(1)
|
||||
go func(entry *zip.File) {
|
||||
defer wg.Done()
|
||||
entryFile, err := entry.Open()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_ = entryFile.Close()
|
||||
}()
|
||||
|
||||
loader.loadFile(entry.Name, false, entryFile)
|
||||
}(file)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func (loader *fileLoader) loadFile(name string, isOnlyStagedFile bool, file io.Reader) {
|
||||
fileData, err := ioutil.ReadAll(file)
|
||||
if err != nil {
|
||||
loader.markFailedFile()
|
||||
}
|
||||
|
||||
reader, err := lgres.ReaderFrom(bytes.NewReader(fileData))
|
||||
filename := filepath.Base(name)
|
||||
if (err == nil) && (isOnlyStagedFile || fileAllowlist.Matches(filename)) {
|
||||
location := FileLocation{DirPath: filepath.Dir(name), Name: filename}
|
||||
loader.modify(func() {
|
||||
if stateView, stateErr := reader.View(ids.GameState); (stateErr == nil) && archive.IsSavegame(stateView) {
|
||||
loader.result.Savegames[location] = reader
|
||||
} else {
|
||||
loader.result.Resources[location] = reader
|
||||
}
|
||||
})
|
||||
}
|
||||
if strings.ToLower(filename) == ObjectPropertiesFilename {
|
||||
decoder := serial.NewDecoder(bytes.NewReader(fileData))
|
||||
properties := object.StandardPropertiesTable()
|
||||
properties.Code(decoder)
|
||||
err = decoder.FirstError()
|
||||
if err == nil {
|
||||
loader.modify(func() { loader.result.ObjectProperties = properties })
|
||||
}
|
||||
}
|
||||
if strings.ToLower(filename) == TexturePropertiesFilename && (len(fileData) > 4) {
|
||||
decoder := serial.NewDecoder(bytes.NewReader(fileData))
|
||||
entryCount := (len(fileData) - 4) / texture.PropertiesSize
|
||||
properties := make(texture.PropertiesList, entryCount)
|
||||
properties.Code(decoder)
|
||||
err = decoder.FirstError()
|
||||
if err == nil {
|
||||
loader.modify(func() { loader.result.TextureProperties = properties })
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
loader.markFailedFile()
|
||||
}
|
||||
}
|
||||
|
||||
func (loader *fileLoader) markFailedFile() {
|
||||
loader.modify(func() { loader.result.FailedFiles++ })
|
||||
}
|
||||
|
||||
func (loader *fileLoader) modify(modifier func()) {
|
||||
loader.resultMutex.Lock()
|
||||
defer loader.resultMutex.Unlock()
|
||||
modifier()
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package world
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// FileLocation describes the path and name of a file.
|
||||
type FileLocation struct {
|
||||
DirPath string
|
||||
Name string
|
||||
}
|
||||
|
||||
// FileLocationFrom returns an instance for given filename.
|
||||
func FileLocationFrom(filename string) FileLocation {
|
||||
return FileLocation{
|
||||
DirPath: filepath.Dir(filename),
|
||||
Name: filepath.Base(filename),
|
||||
}
|
||||
}
|
||||
|
||||
// AbsolutePathFrom returns the complete path, based on given base, of the location.
|
||||
func (loc FileLocation) AbsolutePathFrom(base string) string {
|
||||
rel, err := filepath.Rel(base, loc.DirPath)
|
||||
if err != nil {
|
||||
if filepath.IsAbs(loc.DirPath) {
|
||||
return filepath.Join(loc.DirPath, loc.Name)
|
||||
}
|
||||
return filepath.Join(base, loc.DirPath, loc.Name)
|
||||
}
|
||||
return filepath.Join(base, rel, loc.Name)
|
||||
}
|
||||
|
||||
// NestedRelativeTo returns a string that describes the relative nested location to given base - if possible.
|
||||
func (loc FileLocation) NestedRelativeTo(base string) string {
|
||||
absoluteDir := loc.DirPath
|
||||
if !filepath.IsAbs(absoluteDir) {
|
||||
absoluteDir = filepath.Clean(filepath.Join(base, loc.DirPath))
|
||||
}
|
||||
if !strings.HasPrefix(absoluteDir+string(filepath.Separator), base+string(filepath.Separator)) {
|
||||
return filepath.Join(absoluteDir, loc.Name)
|
||||
}
|
||||
rel, err := filepath.Rel(base, absoluteDir)
|
||||
if err != nil {
|
||||
return filepath.Join(base, loc.Name)
|
||||
}
|
||||
return filepath.Join(".", rel, loc.Name)
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package world_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/world"
|
||||
)
|
||||
|
||||
func TestFileLocationAbsolutePathFrom(t *testing.T) {
|
||||
temp := os.TempDir()
|
||||
base := filepath.Join(temp, "base")
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
loc world.FileLocation
|
||||
base string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "file loaded in same base",
|
||||
loc: world.FileLocation{DirPath: base, Name: "test.res"},
|
||||
base: base,
|
||||
expected: filepath.Join(base, "test.res"),
|
||||
},
|
||||
{
|
||||
name: "new file",
|
||||
loc: world.FileLocation{DirPath: ".", Name: "test.res"},
|
||||
base: base,
|
||||
expected: filepath.Join(base, "test.res"),
|
||||
},
|
||||
{
|
||||
name: "new file without dir path",
|
||||
loc: world.FileLocation{DirPath: "", Name: "test.res"},
|
||||
base: base,
|
||||
expected: filepath.Join(base, "test.res"),
|
||||
},
|
||||
{
|
||||
name: "file found in different sibling path",
|
||||
loc: world.FileLocation{DirPath: filepath.Join(temp, "other"), Name: "test.res"},
|
||||
base: base,
|
||||
expected: filepath.Join(temp, "other", "test.res"),
|
||||
},
|
||||
{
|
||||
name: "file found in sub path",
|
||||
loc: world.FileLocation{DirPath: filepath.Join(base, "nested"), Name: "test.res"},
|
||||
base: base,
|
||||
expected: filepath.Join(base, "nested", "test.res"),
|
||||
},
|
||||
{
|
||||
name: "nested relative file dir path",
|
||||
loc: world.FileLocation{DirPath: "nested", Name: "test.res"},
|
||||
base: base,
|
||||
expected: filepath.Join(base, "nested", "test.res"),
|
||||
},
|
||||
}
|
||||
for _, tc := range tt {
|
||||
func(name string, loc world.FileLocation, base, expected string) {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
abs := loc.AbsolutePathFrom(base)
|
||||
assert.Equal(t, expected, abs)
|
||||
})
|
||||
}(tc.name, tc.loc, tc.base, tc.expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileLocationNestedRelativeTo(t *testing.T) {
|
||||
temp := os.TempDir()
|
||||
base := filepath.Join(temp, "base")
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
loc world.FileLocation
|
||||
base string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "file loaded in same base",
|
||||
loc: world.FileLocation{DirPath: base, Name: "test.res"},
|
||||
base: base,
|
||||
expected: filepath.Join(".", "test.res"),
|
||||
},
|
||||
{
|
||||
name: "new file",
|
||||
loc: world.FileLocation{DirPath: ".", Name: "test.res"},
|
||||
base: base,
|
||||
expected: filepath.Join(".", "test.res"),
|
||||
},
|
||||
{
|
||||
name: "new file without dir path",
|
||||
loc: world.FileLocation{DirPath: "", Name: "test.res"},
|
||||
base: base,
|
||||
expected: filepath.Join(".", "test.res"),
|
||||
},
|
||||
{
|
||||
name: "file found in different path",
|
||||
loc: world.FileLocation{DirPath: filepath.Join(temp, "other"), Name: "test.res"},
|
||||
base: base,
|
||||
expected: filepath.Join(temp, "other", "test.res"),
|
||||
},
|
||||
{
|
||||
name: "file found in sub path",
|
||||
loc: world.FileLocation{DirPath: filepath.Join(base, "nested"), Name: "test.res"},
|
||||
base: base,
|
||||
expected: filepath.Join(".", "nested", "test.res"),
|
||||
},
|
||||
{
|
||||
name: "file found in similar named different path",
|
||||
loc: world.FileLocation{DirPath: filepath.Join(temp, "baseOther"), Name: "test.res"},
|
||||
base: base,
|
||||
expected: filepath.Join(temp, "baseOther", "test.res"),
|
||||
},
|
||||
}
|
||||
for _, tc := range tt {
|
||||
func(name string, loc world.FileLocation, base, expected string) {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
rel := loc.NestedRelativeTo(base)
|
||||
assert.Equal(t, expected, rel)
|
||||
})
|
||||
}(tc.name, tc.loc, tc.base, tc.expected)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
package world
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/hacked/ss1/content/object"
|
||||
"github.com/inkyblackness/hacked/ss1/content/texture"
|
||||
"github.com/inkyblackness/hacked/ss1/resource"
|
||||
)
|
||||
|
||||
// Manifest contains all the data and information of concrete things in a world.
|
||||
type Manifest struct {
|
||||
resourceChangeNotifier resource.ChangeNotifier
|
||||
entries []*ManifestEntry
|
||||
}
|
||||
|
||||
// NewManifest returns a new instance that notifies changes to the provided callback.
|
||||
func NewManifest(modified resource.ModificationCallback) *Manifest {
|
||||
var manifest Manifest
|
||||
|
||||
manifest.resourceChangeNotifier.Callback = modified
|
||||
manifest.resourceChangeNotifier.Localizer = &manifest
|
||||
|
||||
return &manifest
|
||||
}
|
||||
|
||||
// EntryCount returns the number of entries currently in the manifest.
|
||||
func (manifest Manifest) EntryCount() int {
|
||||
return len(manifest.entries)
|
||||
}
|
||||
|
||||
// Entry returns the entry at given index.
|
||||
func (manifest Manifest) Entry(at int) (*ManifestEntry, error) {
|
||||
if (at < 0) || (at >= len(manifest.entries)) {
|
||||
return nil, errIndexOutOfBounds
|
||||
}
|
||||
return manifest.entries[at], nil
|
||||
}
|
||||
|
||||
// Reset clears the manifest by removing all entries and notifying of all the removed resource identifier.
|
||||
func (manifest *Manifest) Reset() {
|
||||
oldEntries := manifest.entries
|
||||
manifest.changeAndNotify(func() {
|
||||
manifest.entries = nil
|
||||
}, manifest.listIDs(oldEntries...))
|
||||
}
|
||||
|
||||
// InsertEntry puts the provided entries in sequence at the specified index.
|
||||
// Any entry at the identified index, and all those after that, are moved behind the given ones.
|
||||
func (manifest *Manifest) InsertEntry(at int, entries ...*ManifestEntry) error {
|
||||
oldLen := len(manifest.entries)
|
||||
if (at < 0) || (at > oldLen) {
|
||||
return errIndexOutOfBounds
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if entry == nil {
|
||||
return errEntryIsNil
|
||||
}
|
||||
}
|
||||
manifest.changeAndNotify(func() {
|
||||
addLen := len(entries)
|
||||
newEntries := make([]*ManifestEntry, oldLen+addLen)
|
||||
copy(newEntries[:at], manifest.entries[:at])
|
||||
copy(newEntries[at:], entries)
|
||||
copy(newEntries[at+addLen:oldLen+addLen], manifest.entries[at:oldLen])
|
||||
manifest.entries = newEntries
|
||||
}, manifest.listIDs(entries...))
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveEntry removes the entry at given index.
|
||||
func (manifest *Manifest) RemoveEntry(at int) error {
|
||||
oldLen := len(manifest.entries)
|
||||
if (at < 0) || (at >= oldLen) {
|
||||
return errIndexOutOfBounds
|
||||
}
|
||||
manifest.changeAndNotify(func() {
|
||||
copy(manifest.entries[at:oldLen-1], manifest.entries[at+1:oldLen])
|
||||
manifest.entries = manifest.entries[:oldLen-1]
|
||||
}, manifest.listIDs(manifest.entries[at]))
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReplaceEntry removes the current entry at the identified index and puts the provided one instead.
|
||||
func (manifest *Manifest) ReplaceEntry(at int, entry *ManifestEntry) error {
|
||||
if (at < 0) || (at >= len(manifest.entries)) {
|
||||
return errIndexOutOfBounds
|
||||
}
|
||||
if entry == nil {
|
||||
return errEntryIsNil
|
||||
}
|
||||
manifest.changeAndNotify(func() {
|
||||
manifest.entries[at] = entry
|
||||
}, manifest.listIDs(manifest.entries[at]), manifest.listIDs(entry))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MoveEntry removes an entry and reinserts it at another index.
|
||||
// Both indices are resolved before the move.
|
||||
func (manifest *Manifest) MoveEntry(to, from int) error {
|
||||
curLen := len(manifest.entries)
|
||||
if (to < 0) || (to >= curLen) || (from < 0) || (from >= curLen) {
|
||||
return errIndexOutOfBounds
|
||||
}
|
||||
manifest.changeAndNotify(func() {
|
||||
entry := manifest.entries[from]
|
||||
if from >= to {
|
||||
copy(manifest.entries[to+1:from+1], manifest.entries[to:from])
|
||||
} else {
|
||||
copy(manifest.entries[from:to], manifest.entries[from+1:to+1])
|
||||
}
|
||||
manifest.entries[to] = entry
|
||||
}, manifest.listIDs(manifest.entries[from]))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Filter finds all resources in the world that match the given parameters.
|
||||
func (manifest Manifest) Filter(lang resource.Language, id resource.ID) resource.List {
|
||||
var list resource.List
|
||||
for _, entry := range manifest.entries {
|
||||
list = list.Joined(entry.Resources.Filter(lang, id))
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
// LocalizedResources produces a selector to retrieve resources for a specific language from the manifest.
|
||||
// The returned selector has the strategy to merge the typical compound resource lists, such
|
||||
// as the small textures, or string lookups. It is based on StandardResourceViewStrategy().
|
||||
func (manifest *Manifest) LocalizedResources(lang resource.Language) resource.Selector {
|
||||
return resource.Selector{
|
||||
Lang: lang,
|
||||
From: manifest,
|
||||
As: ResourceViewStrategy(),
|
||||
}
|
||||
}
|
||||
|
||||
// ObjectProperties returns the table of object properties.
|
||||
func (manifest *Manifest) ObjectProperties() object.PropertiesTable {
|
||||
var table object.PropertiesTable
|
||||
for _, entry := range manifest.entries {
|
||||
if len(entry.ObjectProperties) > 0 {
|
||||
table = entry.ObjectProperties
|
||||
}
|
||||
}
|
||||
return table
|
||||
}
|
||||
|
||||
// TextureProperties returns the table of texture properties.
|
||||
func (manifest *Manifest) TextureProperties() texture.PropertiesList {
|
||||
var list texture.PropertiesList
|
||||
for _, entry := range manifest.entries {
|
||||
if len(entry.TextureProperties) > 0 {
|
||||
list = entry.TextureProperties
|
||||
}
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
func (manifest *Manifest) listIDs(entries ...*ManifestEntry) (ids []resource.ID) {
|
||||
for _, entry := range entries {
|
||||
for _, res := range entry.Resources {
|
||||
singleIDs := res.Viewer.IDs()
|
||||
ids = append(ids, singleIDs...)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (manifest *Manifest) changeAndNotify(modifier func(), affectedIDs ...[]resource.ID) {
|
||||
manifest.resourceChangeNotifier.ModifyAndNotify(modifier, affectedIDs...)
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package world
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/hacked/ss1/content/object"
|
||||
"github.com/inkyblackness/hacked/ss1/content/texture"
|
||||
"github.com/inkyblackness/hacked/ss1/resource"
|
||||
"github.com/inkyblackness/hacked/ss1/world/ids"
|
||||
)
|
||||
|
||||
// ManifestEntry describes a set of localized resources under a collective identifier.
|
||||
type ManifestEntry struct {
|
||||
Origin []string
|
||||
ID string
|
||||
Resources resource.LocalizedResourcesList
|
||||
|
||||
ObjectProperties object.PropertiesTable
|
||||
TextureProperties texture.PropertiesList
|
||||
}
|
||||
|
||||
// NewManifestEntryFrom attempts to create a manifest in memory from the given set of files.
|
||||
// It uses LoadFiles() to load the files with given filenames into memory, allowing archives as well.
|
||||
func NewManifestEntryFrom(names []string) (*ManifestEntry, error) {
|
||||
loaded := LoadFiles(true, names)
|
||||
|
||||
if len(loaded.Resources) == 0 {
|
||||
return nil, errNoResourcesFound
|
||||
}
|
||||
|
||||
entry := &ManifestEntry{
|
||||
Origin: names,
|
||||
ID: names[0],
|
||||
}
|
||||
|
||||
for location, viewer := range loaded.Resources {
|
||||
localized := resource.LocalizedResources{
|
||||
ID: location.Name,
|
||||
Language: ids.LocalizeFilename(location.Name),
|
||||
Viewer: viewer,
|
||||
}
|
||||
entry.Resources = append(entry.Resources, localized)
|
||||
}
|
||||
entry.ObjectProperties = loaded.ObjectProperties
|
||||
entry.TextureProperties = loaded.TextureProperties
|
||||
return entry, nil
|
||||
}
|
||||
|
||||
// LocalizedResources produces a selector to retrieve resources for a specific language from this entry.
|
||||
func (entry ManifestEntry) LocalizedResources(lang resource.Language) resource.Selector {
|
||||
return resource.Selector{
|
||||
From: entry.Resources,
|
||||
Lang: lang,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package world_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/resource"
|
||||
"github.com/inkyblackness/hacked/ss1/world"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type ManifestEntrySuite struct {
|
||||
suite.Suite
|
||||
entry *world.ManifestEntry
|
||||
finder resource.Selector
|
||||
}
|
||||
|
||||
func TestManifestEntrySuite(t *testing.T) {
|
||||
suite.Run(t, new(ManifestEntrySuite))
|
||||
}
|
||||
|
||||
func (suite *ManifestEntrySuite) SetupTest() {
|
||||
suite.entry = new(world.ManifestEntry)
|
||||
}
|
||||
|
||||
func (suite *ManifestEntrySuite) TestResourceReturnsErrorIfNothingFound() {
|
||||
suite.whenViewingResource(resource.LangAny)
|
||||
suite.thenResourceViewShouldNotBeAvailable(1234)
|
||||
}
|
||||
|
||||
func (suite *ManifestEntrySuite) TestResourceReturnsViewIfMatched() {
|
||||
suite.givenLocalizedResources(resource.LangAny, 1000, [][]byte{{0x01}})
|
||||
suite.whenViewingResource(resource.LangAny)
|
||||
suite.thenResourceViewShouldBeAvailable(1000)
|
||||
}
|
||||
|
||||
func (suite *ManifestEntrySuite) TestResourceConsidersResourcesWithLanguageAnyWhenLookingForSpecific() {
|
||||
suite.givenLocalizedResources(resource.LangAny, 1000, [][]byte{{0x01}})
|
||||
suite.whenViewingResource(resource.LangDefault)
|
||||
suite.thenResourceViewShouldBeAvailable(1000)
|
||||
}
|
||||
|
||||
func (suite *ManifestEntrySuite) TestResourceConsidersResourcesWithSpecificLanguage() {
|
||||
suite.givenLocalizedResources(resource.LangFrench, 1000, [][]byte{{0x01}})
|
||||
suite.whenViewingResource(resource.LangFrench)
|
||||
suite.thenResourceViewShouldBeAvailable(1000)
|
||||
}
|
||||
|
||||
func (suite *ManifestEntrySuite) TestResourceIgnoresResourcesWithSpecificLanguageWhenLookingForAny() {
|
||||
suite.givenLocalizedResources(resource.LangFrench, 1000, [][]byte{{0x01}})
|
||||
suite.whenViewingResource(resource.LangAny)
|
||||
suite.thenResourceViewShouldNotBeAvailable(1000)
|
||||
}
|
||||
|
||||
func (suite *ManifestEntrySuite) TestResourceIgnoresResourcesWithSpecificLanguageWhenLookingForDifferentLanguage() {
|
||||
suite.givenLocalizedResources(resource.LangFrench, 1000, [][]byte{{0x01}})
|
||||
suite.whenViewingResource(resource.LangDefault)
|
||||
suite.thenResourceViewShouldNotBeAvailable(1000)
|
||||
|
||||
suite.whenViewingResource(resource.LangGerman)
|
||||
suite.thenResourceViewShouldNotBeAvailable(1000)
|
||||
}
|
||||
|
||||
func (suite *ManifestEntrySuite) givenLocalizedResources(lang resource.Language, id int, data [][]byte) {
|
||||
var store resource.Store
|
||||
_ = store.Put(resource.ID(id), resource.Resource{
|
||||
Blocks: resource.BlocksFrom(data),
|
||||
})
|
||||
suite.entry.Resources = append(suite.entry.Resources, resource.LocalizedResources{
|
||||
Language: lang,
|
||||
Viewer: store,
|
||||
})
|
||||
}
|
||||
|
||||
func (suite *ManifestEntrySuite) whenViewingResource(lang resource.Language) {
|
||||
suite.finder = suite.entry.LocalizedResources(lang)
|
||||
}
|
||||
|
||||
func (suite *ManifestEntrySuite) thenResourceViewShouldNotBeAvailable(id int) {
|
||||
view, err := suite.finder.Select(resource.ID(id))
|
||||
assert.Error(suite.T(), err, fmt.Sprintf("Error expected finding resource %v", id))
|
||||
assert.Nil(suite.T(), view, fmt.Sprintf("No view expected finding resource %v", id))
|
||||
}
|
||||
|
||||
func (suite *ManifestEntrySuite) thenResourceViewShouldBeAvailable(id int) {
|
||||
view, err := suite.finder.Select(resource.ID(id))
|
||||
require.Nil(suite.T(), err, fmt.Sprintf("No error expected finding resource %v", id))
|
||||
assert.NotNil(suite.T(), view, fmt.Sprintf("View expected for resource %v", id))
|
||||
}
|
||||
@@ -0,0 +1,478 @@
|
||||
package world_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/resource"
|
||||
"github.com/inkyblackness/hacked/ss1/world"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type ManifestSuite struct {
|
||||
suite.Suite
|
||||
manifest *world.Manifest
|
||||
selector *resource.Selector
|
||||
|
||||
lastModifiedIDs []resource.ID
|
||||
lastFailedIDs []resource.ID
|
||||
}
|
||||
|
||||
func TestManifestSuite(t *testing.T) {
|
||||
suite.Run(t, new(ManifestSuite))
|
||||
}
|
||||
|
||||
func (suite *ManifestSuite) SetupTest() {
|
||||
suite.manifest = world.NewManifest(suite.onManifestModified)
|
||||
suite.selector = nil
|
||||
|
||||
suite.lastModifiedIDs = nil
|
||||
suite.lastFailedIDs = nil
|
||||
}
|
||||
|
||||
func (suite *ManifestSuite) TestEntriesCanBeAdded() {
|
||||
suite.whenEntryIsInserted(0, "id1")
|
||||
suite.thenEntryCountShouldBe(1)
|
||||
suite.thenEntryAtShouldBe(0, "id1")
|
||||
}
|
||||
|
||||
func (suite *ManifestSuite) TestMultipleEntriesCanBeAdded() {
|
||||
suite.whenEntriesAreInserted(0, "id1", "id2", "id3")
|
||||
suite.thenEntryCountShouldBe(3)
|
||||
suite.thenEntryAtShouldBe(0, "id1")
|
||||
suite.thenEntryAtShouldBe(1, "id2")
|
||||
suite.thenEntryAtShouldBe(2, "id3")
|
||||
}
|
||||
|
||||
func (suite *ManifestSuite) TestEntryReturnsErrorOnInvalidInput() {
|
||||
suite.givenEntryWasInserted(0, "id1")
|
||||
|
||||
assertErrorAt := func(at int, message string) {
|
||||
_, err := suite.manifest.Entry(at)
|
||||
assert.Error(suite.T(), err, message)
|
||||
}
|
||||
assertErrorAt(-1, "Error expected with negative index")
|
||||
assertErrorAt(1, "Error expected with index beyond limit")
|
||||
}
|
||||
|
||||
func (suite *ManifestSuite) TestEntriesCanBeInserted() {
|
||||
suite.givenEntryWasInserted(0, "id1")
|
||||
suite.givenEntryWasInserted(1, "id2")
|
||||
suite.whenEntryIsInserted(1, "id3")
|
||||
suite.thenEntryCountShouldBe(3)
|
||||
suite.thenEntryAtShouldBe(0, "id1")
|
||||
suite.thenEntryAtShouldBe(1, "id3")
|
||||
suite.thenEntryAtShouldBe(2, "id2")
|
||||
}
|
||||
|
||||
func (suite *ManifestSuite) TestMultipleEntriesCanBeInserted() {
|
||||
suite.givenEntryWasInserted(0, "id1")
|
||||
suite.givenEntryWasInserted(1, "id2")
|
||||
suite.whenEntriesAreInserted(1, "id3", "id4")
|
||||
suite.thenEntryCountShouldBe(4)
|
||||
suite.thenEntryAtShouldBe(0, "id1")
|
||||
suite.thenEntryAtShouldBe(1, "id3")
|
||||
suite.thenEntryAtShouldBe(2, "id4")
|
||||
suite.thenEntryAtShouldBe(3, "id2")
|
||||
}
|
||||
|
||||
func (suite *ManifestSuite) TestInsertReturnsErrorsOnInvalidData() {
|
||||
assert.Error(suite.T(), suite.manifest.InsertEntry(-1, suite.aSimpleEntry("-1")), "Error expected with negative index")
|
||||
assert.Error(suite.T(), suite.manifest.InsertEntry(1, suite.aSimpleEntry("1")), "Error expected with index beyond count")
|
||||
assert.Error(suite.T(), suite.manifest.InsertEntry(0, nil), "Error expected for nil entry")
|
||||
assert.Error(suite.T(), suite.manifest.InsertEntry(0, suite.aSimpleEntry("1"), nil, suite.aSimpleEntry("2")), "Error expected for nil entry")
|
||||
}
|
||||
|
||||
func (suite *ManifestSuite) TestEntriesCanBeRemoved() {
|
||||
suite.givenEntryWasInserted(0, "id1")
|
||||
suite.givenEntryWasInserted(1, "id2")
|
||||
suite.givenEntryWasInserted(2, "id3")
|
||||
suite.whenEntryIsRemoved(1)
|
||||
suite.thenEntryCountShouldBe(2)
|
||||
suite.thenEntryAtShouldBe(0, "id1")
|
||||
suite.thenEntryAtShouldBe(1, "id3")
|
||||
|
||||
suite.whenEntryIsRemoved(1)
|
||||
suite.thenEntryCountShouldBe(1)
|
||||
suite.thenEntryAtShouldBe(0, "id1")
|
||||
}
|
||||
|
||||
func (suite *ManifestSuite) TestRemoveReturnsErrorOnInvalidData() {
|
||||
suite.givenEntryWasInserted(0, "id1")
|
||||
|
||||
assert.Error(suite.T(), suite.manifest.RemoveEntry(-1), "Error expected with negative index")
|
||||
assert.Error(suite.T(), suite.manifest.RemoveEntry(1), "Error expected with index beyond count")
|
||||
}
|
||||
|
||||
func (suite *ManifestSuite) TestEntriesCanBeReplaced() {
|
||||
suite.givenEntryWasInserted(0, "id1")
|
||||
suite.whenEntryIsReplaced(0, "id1-new")
|
||||
suite.thenEntryAtShouldBe(0, "id1-new")
|
||||
}
|
||||
|
||||
func (suite *ManifestSuite) TestReplaceReturnsErrorOnInvalidData() {
|
||||
suite.givenEntryWasInserted(0, "id1")
|
||||
|
||||
assert.Error(suite.T(), suite.manifest.ReplaceEntry(-1, suite.aSimpleEntry("-1")), "Error expected with negative index")
|
||||
assert.Error(suite.T(), suite.manifest.ReplaceEntry(1, suite.aSimpleEntry("1")), "Error expected with index beyond count")
|
||||
assert.Error(suite.T(), suite.manifest.ReplaceEntry(0, nil), "Error expected for nil entry")
|
||||
}
|
||||
|
||||
func (suite *ManifestSuite) TestEntriesCanBeMovedBackward() {
|
||||
suite.givenEntryWasInserted(0, "id1")
|
||||
suite.givenEntryWasInserted(1, "id2")
|
||||
suite.givenEntryWasInserted(2, "id3")
|
||||
suite.givenEntryWasInserted(3, "id4")
|
||||
suite.givenEntryWasInserted(4, "id5")
|
||||
suite.whenEntryIsMoved(1, 3)
|
||||
suite.thenEntryOrderShouldBe("id1", "id4", "id2", "id3", "id5")
|
||||
}
|
||||
|
||||
func (suite *ManifestSuite) TestEntriesCanBeMovedForward() {
|
||||
suite.givenEntryWasInserted(0, "id1")
|
||||
suite.givenEntryWasInserted(1, "id2")
|
||||
suite.givenEntryWasInserted(2, "id3")
|
||||
suite.givenEntryWasInserted(3, "id4")
|
||||
suite.givenEntryWasInserted(4, "id5")
|
||||
suite.whenEntryIsMoved(3, 1)
|
||||
suite.thenEntryOrderShouldBe("id1", "id3", "id4", "id2", "id5")
|
||||
}
|
||||
|
||||
func (suite *ManifestSuite) TestMoveEntryToSamePlaceDoesNothing() {
|
||||
suite.givenEntryWasInserted(0, "id1")
|
||||
suite.givenEntryWasInserted(1, "id2")
|
||||
suite.givenEntryWasInserted(2, "id3")
|
||||
suite.whenEntryIsMoved(1, 1)
|
||||
suite.thenEntryOrderShouldBe("id1", "id2", "id3")
|
||||
}
|
||||
|
||||
func (suite *ManifestSuite) TestMoveEntryToEnd() {
|
||||
suite.givenEntryWasInserted(0, "id1")
|
||||
suite.givenEntryWasInserted(1, "id2")
|
||||
suite.givenEntryWasInserted(2, "id3")
|
||||
suite.whenEntryIsMoved(2, 0)
|
||||
suite.thenEntryOrderShouldBe("id2", "id3", "id1")
|
||||
}
|
||||
|
||||
func (suite *ManifestSuite) TestMoveEntryReturnsErrorOnInvalidInput() {
|
||||
suite.givenEntryWasInserted(0, "id1")
|
||||
suite.givenEntryWasInserted(1, "id2")
|
||||
suite.givenEntryWasInserted(2, "id3")
|
||||
|
||||
assert.Error(suite.T(), suite.manifest.MoveEntry(0, -1), "Error expected moving from negative index")
|
||||
assert.Error(suite.T(), suite.manifest.MoveEntry(0, 3), "Error expected moving from index beyond limit")
|
||||
assert.Error(suite.T(), suite.manifest.MoveEntry(-1, 1), "Error expected moving to negative index")
|
||||
assert.Error(suite.T(), suite.manifest.MoveEntry(3, 1), "Error expected moving to index beyond limit")
|
||||
}
|
||||
|
||||
func (suite *ManifestSuite) TestLocalizedResourcesCanBeRetrieved() {
|
||||
suite.givenEntryWasInsertedWith(0, "id1",
|
||||
suite.someLocalizedResources(resource.LangAny,
|
||||
suite.storing(0x1234, [][]byte{{0xAA}})))
|
||||
suite.givenEntryWasInserted(1, "id2")
|
||||
suite.whenResourcesAreQueriedFor(resource.LangAny)
|
||||
suite.thenResourcesCanBeSelected(0x1234)
|
||||
}
|
||||
|
||||
func (suite *ManifestSuite) TestModifiedCallbackOnInsertFromEmptyListsNewIDs() {
|
||||
suite.whenEntryIsInsertedWith(0, "id1",
|
||||
suite.someLocalizedResources(resource.LangAny,
|
||||
suite.storing(0x1000, [][]byte{{0x11}}),
|
||||
suite.storing(0x2000, [][]byte{{0x22}})))
|
||||
suite.thenModifiedResourcesShouldBe([]int{0x1000, 0x2000})
|
||||
}
|
||||
|
||||
func (suite *ManifestSuite) TestModifiedCallbackOnInsertFromEmptyListsNewIDsMultipleEntries() {
|
||||
suite.whenEntriesAreInsertedWith(0, 10,
|
||||
suite.someLocalizedResources(resource.LangAny,
|
||||
suite.storing(0x1000, [][]byte{{0x11}}),
|
||||
suite.storing(0x2000, [][]byte{{0x22}})),
|
||||
suite.someLocalizedResources(resource.LangAny,
|
||||
suite.storing(0x3000, [][]byte{{0x11}}),
|
||||
suite.storing(0x4000, [][]byte{{0x22}})))
|
||||
suite.thenModifiedResourcesShouldBe([]int{0x1000, 0x2000, 0x3000, 0x4000})
|
||||
}
|
||||
|
||||
func (suite *ManifestSuite) TestModifiedCallbackOnInsertWithNewOnesListsNewIDs() {
|
||||
suite.givenEntryWasInsertedWith(0, "id1",
|
||||
suite.someLocalizedResources(resource.LangAny,
|
||||
suite.storing(0x0800, [][]byte{{0xAA}})))
|
||||
suite.whenEntryIsInsertedWith(1, "id2",
|
||||
suite.someLocalizedResources(resource.LangAny,
|
||||
suite.storing(0x0900, [][]byte{{0x11}}),
|
||||
suite.storing(0x0A00, [][]byte{{0x22}})))
|
||||
suite.thenModifiedResourcesShouldBe([]int{0x0900, 0x0A00})
|
||||
}
|
||||
|
||||
func (suite *ManifestSuite) TestModifiedCallbackOnInsertIgnoresIdenticalData() {
|
||||
suite.givenEntryWasInsertedWith(0, "id1",
|
||||
suite.someLocalizedResources(resource.LangAny,
|
||||
suite.storing(0x0800, [][]byte{{0xAA}})))
|
||||
suite.whenEntryIsInsertedWith(1, "id2",
|
||||
suite.someLocalizedResources(resource.LangAny,
|
||||
suite.storing(0x0800, [][]byte{{0xAA}})))
|
||||
suite.thenModifiedResourcesShouldBe([]int{})
|
||||
}
|
||||
|
||||
func (suite *ManifestSuite) TestModifiedCallbackOnInsertWithChangedData() {
|
||||
suite.givenEntryWasInsertedWith(0, "id1",
|
||||
suite.someLocalizedResources(resource.LangAny,
|
||||
suite.storing(0x0800, [][]byte{{0xAA}})))
|
||||
suite.whenEntryIsInsertedWith(1, "id2",
|
||||
suite.someLocalizedResources(resource.LangAny,
|
||||
suite.storing(0x0800, [][]byte{{0xBB}})))
|
||||
suite.thenModifiedResourcesShouldBe([]int{0x0800})
|
||||
}
|
||||
|
||||
func (suite *ManifestSuite) TestModifiedCallbackOnRemoveWithLostIDs() {
|
||||
suite.givenEntryWasInsertedWith(0, "id1",
|
||||
suite.someLocalizedResources(resource.LangAny,
|
||||
suite.storing(0x0800, [][]byte{{0xAA}})))
|
||||
suite.givenEntryWasInsertedWith(1, "id1",
|
||||
suite.someLocalizedResources(resource.LangAny,
|
||||
suite.storing(0x0900, [][]byte{{0xBB}})))
|
||||
suite.whenEntryIsRemoved(0)
|
||||
suite.thenModifiedResourcesShouldBe([]int{0x0800})
|
||||
}
|
||||
|
||||
func (suite *ManifestSuite) TestModifiedCallbackOnRemoveIgnoresIdenticalData_RemovedBefore() {
|
||||
suite.givenEntryWasInsertedWith(0, "id1",
|
||||
suite.someLocalizedResources(resource.LangAny,
|
||||
suite.storing(0x0800, [][]byte{{0xAA}, {0xBB}})))
|
||||
suite.givenEntryWasInsertedWith(1, "id2",
|
||||
suite.someLocalizedResources(resource.LangAny,
|
||||
suite.storing(0x0800, [][]byte{{0xAA}, {0xBB}})))
|
||||
suite.whenEntryIsRemoved(0)
|
||||
suite.thenModifiedResourcesShouldBe([]int{})
|
||||
}
|
||||
|
||||
func (suite *ManifestSuite) TestModifiedCallbackOnRemoveIgnoresIdenticalData_RemovedAfter() {
|
||||
suite.givenEntryWasInsertedWith(0, "id1",
|
||||
suite.someLocalizedResources(resource.LangAny,
|
||||
suite.storing(0x0800, [][]byte{{0xAA}, {0xBB}})))
|
||||
suite.givenEntryWasInsertedWith(1, "id2",
|
||||
suite.someLocalizedResources(resource.LangAny,
|
||||
suite.storing(0x0800, [][]byte{{0xAA}, {0xBB}})))
|
||||
suite.whenEntryIsRemoved(1)
|
||||
suite.thenModifiedResourcesShouldBe([]int{})
|
||||
}
|
||||
|
||||
func (suite *ManifestSuite) TestModifiedCallbackOnRemoveWithChangedData() {
|
||||
suite.givenEntryWasInsertedWith(0, "id1",
|
||||
suite.someLocalizedResources(resource.LangAny,
|
||||
suite.storing(0x0800, [][]byte{{0xAA}})))
|
||||
suite.givenEntryWasInsertedWith(1, "id2",
|
||||
suite.someLocalizedResources(resource.LangAny,
|
||||
suite.storing(0x0800, [][]byte{{0xBB}})))
|
||||
suite.whenEntryIsRemoved(1)
|
||||
suite.thenModifiedResourcesShouldBe([]int{0x0800})
|
||||
}
|
||||
|
||||
func (suite *ManifestSuite) TestModifiedCallbackOnReplaceWithChangedData() {
|
||||
suite.givenEntryWasInsertedWith(0, "id1",
|
||||
suite.someLocalizedResources(resource.LangAny,
|
||||
suite.storing(0x0800, [][]byte{{0xAA}})))
|
||||
suite.whenEntryIsReplacedWith(0, "id2",
|
||||
suite.someLocalizedResources(resource.LangAny,
|
||||
suite.storing(0x0800, [][]byte{{0xBB}})))
|
||||
suite.thenModifiedResourcesShouldBe([]int{0x0800})
|
||||
}
|
||||
|
||||
func (suite *ManifestSuite) TestModifiedCallbackWithEmptyListOnIdenticalData() {
|
||||
suite.givenEntryWasInsertedWith(0, "id1",
|
||||
suite.someLocalizedResources(resource.LangAny,
|
||||
suite.storing(0x0800, [][]byte{{0xAA}})))
|
||||
suite.whenEntryIsReplacedWith(0, "id2",
|
||||
suite.someLocalizedResources(resource.LangAny,
|
||||
suite.storing(0x0800, [][]byte{{0xAA}})))
|
||||
suite.thenModifiedResourcesShouldBe([]int{})
|
||||
}
|
||||
|
||||
func (suite *ManifestSuite) TestModifiedCallbackWithChangedIDsOnReplace() {
|
||||
suite.givenEntryWasInsertedWith(0, "id1",
|
||||
suite.someLocalizedResources(resource.LangAny,
|
||||
suite.storing(0x0800, [][]byte{{0xAA}})))
|
||||
suite.whenEntryIsReplacedWith(0, "id2",
|
||||
suite.someLocalizedResources(resource.LangAny,
|
||||
suite.storing(0x0900, [][]byte{{0xAA}})))
|
||||
suite.thenModifiedResourcesShouldBe([]int{0x0800, 0x0900})
|
||||
}
|
||||
|
||||
func (suite *ManifestSuite) TestModifiedCallbackWithChangedIDsOnMove() {
|
||||
suite.givenEntryWasInsertedWith(0, "id1",
|
||||
suite.someLocalizedResources(resource.LangAny,
|
||||
suite.storing(0x0800, [][]byte{{0xAA}})))
|
||||
suite.givenEntryWasInsertedWith(1, "id2",
|
||||
suite.someLocalizedResources(resource.LangAny,
|
||||
suite.storing(0x0800, [][]byte{{0xBB}})))
|
||||
suite.givenEntryWasInsertedWith(2, "id3",
|
||||
suite.someLocalizedResources(resource.LangAny,
|
||||
suite.storing(0x0900, [][]byte{{0x11}})))
|
||||
suite.whenEntryIsMoved(2, 0)
|
||||
suite.thenModifiedResourcesShouldBe([]int{0x0800})
|
||||
}
|
||||
|
||||
func (suite *ManifestSuite) TestReset() {
|
||||
suite.givenEntryWasInsertedWith(0, "id1",
|
||||
suite.someLocalizedResources(resource.LangAny,
|
||||
suite.storing(0x0800, [][]byte{{0xAA}})))
|
||||
suite.givenEntryWasInsertedWith(1, "id2",
|
||||
suite.someLocalizedResources(resource.LangAny,
|
||||
suite.storing(0x0900, [][]byte{{0xBB}})))
|
||||
suite.whenManifestIsReset()
|
||||
suite.thenModifiedResourcesShouldBe([]int{0x0800, 0x0900})
|
||||
suite.thenEntryCountShouldBe(0)
|
||||
}
|
||||
|
||||
func (suite *ManifestSuite) onManifestModified(modifiedIDs []resource.ID, failedIDs []resource.ID) {
|
||||
suite.lastModifiedIDs = modifiedIDs
|
||||
suite.lastFailedIDs = failedIDs
|
||||
}
|
||||
|
||||
func (suite *ManifestSuite) givenEntryWasInserted(at int, id string) {
|
||||
suite.whenEntryIsInserted(at, id)
|
||||
}
|
||||
|
||||
func (suite *ManifestSuite) givenEntryWasInsertedWith(at int, id string, res ...resource.LocalizedResources) {
|
||||
suite.whenEntryIsInsertedWith(at, id, res...)
|
||||
suite.lastModifiedIDs = nil
|
||||
suite.lastFailedIDs = nil
|
||||
}
|
||||
|
||||
func (suite *ManifestSuite) whenEntryIsInsertedWith(at int, id string, res ...resource.LocalizedResources) {
|
||||
err := suite.manifest.InsertEntry(at, suite.anEntryWithResources(id, res...))
|
||||
require.Nil(suite.T(), err, fmt.Sprintf("No error expected inserting entry at %d", at))
|
||||
}
|
||||
|
||||
func (suite *ManifestSuite) whenEntriesAreInsertedWith(at int, idBase int, resList ...resource.LocalizedResources) {
|
||||
entries := make([]*world.ManifestEntry, len(resList))
|
||||
for index, res := range resList {
|
||||
entries[index] = suite.anEntryWithResources(fmt.Sprintf("id%d", idBase+index), res)
|
||||
}
|
||||
err := suite.manifest.InsertEntry(at, entries...)
|
||||
require.Nil(suite.T(), err, fmt.Sprintf("No error expected inserting entry at %d", at))
|
||||
}
|
||||
|
||||
func (suite *ManifestSuite) whenEntryIsInserted(at int, id string) {
|
||||
err := suite.manifest.InsertEntry(at, suite.aSimpleEntry(id))
|
||||
require.Nil(suite.T(), err, fmt.Sprintf("No error expected inserting entry at %d", at))
|
||||
}
|
||||
|
||||
func (suite *ManifestSuite) whenEntriesAreInserted(at int, ids ...string) {
|
||||
entries := make([]*world.ManifestEntry, len(ids))
|
||||
for index, id := range ids {
|
||||
entries[index] = suite.aSimpleEntry(id)
|
||||
}
|
||||
err := suite.manifest.InsertEntry(at, entries...)
|
||||
require.Nil(suite.T(), err, fmt.Sprintf("No error expected inserting entry at %d", at))
|
||||
}
|
||||
|
||||
func (suite *ManifestSuite) whenEntryIsRemoved(at int) {
|
||||
err := suite.manifest.RemoveEntry(at)
|
||||
require.Nil(suite.T(), err, fmt.Sprintf("No error expected removing entry at %d", at))
|
||||
}
|
||||
|
||||
func (suite *ManifestSuite) whenEntryIsReplaced(at int, id string) {
|
||||
err := suite.manifest.ReplaceEntry(at, suite.aSimpleEntry(id))
|
||||
require.Nil(suite.T(), err, fmt.Sprintf("No error expected replacing entry at %d", at))
|
||||
}
|
||||
|
||||
func (suite *ManifestSuite) whenEntryIsReplacedWith(at int, id string, res ...resource.LocalizedResources) {
|
||||
err := suite.manifest.ReplaceEntry(at, suite.anEntryWithResources(id, res...))
|
||||
require.Nil(suite.T(), err, fmt.Sprintf("No error expected replacing entry at %d", at))
|
||||
}
|
||||
|
||||
func (suite *ManifestSuite) whenEntryIsMoved(to int, from int) {
|
||||
err := suite.manifest.MoveEntry(to, from)
|
||||
require.Nil(suite.T(), err, fmt.Sprintf("No error expected moving entry to %d from %d", to, from))
|
||||
}
|
||||
|
||||
func (suite *ManifestSuite) whenResourcesAreQueriedFor(lang resource.Language) {
|
||||
selector := suite.manifest.LocalizedResources(lang)
|
||||
suite.selector = &selector
|
||||
}
|
||||
|
||||
func (suite *ManifestSuite) whenManifestIsReset() {
|
||||
suite.manifest.Reset()
|
||||
}
|
||||
|
||||
func (suite *ManifestSuite) thenEntryCountShouldBe(expected int) {
|
||||
assert.Equal(suite.T(), expected, suite.manifest.EntryCount())
|
||||
}
|
||||
|
||||
func (suite *ManifestSuite) thenEntryAtShouldBe(at int, id string) {
|
||||
entry, err := suite.manifest.Entry(at)
|
||||
assert.Nil(suite.T(), err, fmt.Sprintf("No error expected retrieving entry at %d", at))
|
||||
require.NotNil(suite.T(), entry, fmt.Sprintf("Entry expected at %d", at))
|
||||
assert.Equal(suite.T(), id, entry.ID, fmt.Sprintf("Wrong entry found at %d", at))
|
||||
}
|
||||
|
||||
func (suite *ManifestSuite) thenEntryOrderShouldBe(expected ...string) {
|
||||
expectedLen := len(expected)
|
||||
require.Equal(suite.T(), expectedLen, suite.manifest.EntryCount(), "Invalid number of entries")
|
||||
currentIDs := make([]string, expectedLen)
|
||||
for index := 0; index < expectedLen; index++ {
|
||||
entry, err := suite.manifest.Entry(index)
|
||||
require.Nil(suite.T(), err, fmt.Sprintf("No error expected retrieving entry at %d", index))
|
||||
currentIDs[index] = entry.ID
|
||||
}
|
||||
assert.Equal(suite.T(), expected, currentIDs, "IDs don't not match")
|
||||
}
|
||||
|
||||
func (suite *ManifestSuite) thenResourcesCanBeSelected(id int) {
|
||||
view, err := suite.selector.Select(resource.ID(id))
|
||||
assert.Nil(suite.T(), err, "No error expected")
|
||||
assert.NotNil(suite.T(), view, "View expected")
|
||||
}
|
||||
|
||||
func (suite *ManifestSuite) aSimpleEntry(id string) *world.ManifestEntry {
|
||||
return &world.ManifestEntry{
|
||||
ID: id,
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *ManifestSuite) someLocalizedResources(lang resource.Language, modifiers ...func(*resource.Store)) resource.LocalizedResources {
|
||||
var store resource.Store
|
||||
for _, modifier := range modifiers {
|
||||
modifier(&store)
|
||||
}
|
||||
return resource.LocalizedResources{
|
||||
ID: "unnamed",
|
||||
Language: lang,
|
||||
Viewer: store,
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *ManifestSuite) anEntryWithResources(id string, res ...resource.LocalizedResources) *world.ManifestEntry {
|
||||
return &world.ManifestEntry{
|
||||
ID: id,
|
||||
Resources: res,
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *ManifestSuite) storing(id int, data [][]byte) func(*resource.Store) {
|
||||
return func(store *resource.Store) {
|
||||
_ = store.Put(resource.ID(id), resource.Resource{
|
||||
Blocks: resource.BlocksFrom(data),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *ManifestSuite) thenModifiedResourcesShouldBe(expected []int) {
|
||||
identified := make([]resource.ID, len(expected))
|
||||
for index, id := range expected {
|
||||
identified[index] = resource.ID(id)
|
||||
}
|
||||
suite.sortIDs(identified)
|
||||
suite.sortIDs(suite.lastModifiedIDs)
|
||||
|
||||
assert.Equal(suite.T(), identified, suite.lastModifiedIDs, "Modified IDs don't match")
|
||||
}
|
||||
|
||||
func (suite *ManifestSuite) sortIDs(ids []resource.ID) {
|
||||
sort.Slice(ids, func(a, b int) bool { return ids[a] < ids[b] })
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
package world
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/content/object"
|
||||
"github.com/inkyblackness/hacked/ss1/content/texture"
|
||||
"github.com/inkyblackness/hacked/ss1/resource"
|
||||
"github.com/inkyblackness/hacked/ss1/serial/rle"
|
||||
"github.com/inkyblackness/hacked/ss1/world/ids"
|
||||
)
|
||||
|
||||
// ModResetCallback is called when the mod was reset.
|
||||
type ModResetCallback func()
|
||||
|
||||
// Mod is the central object for a game-mod.
|
||||
//
|
||||
// It is based on a "static" world and adds its own changes. The world data itself is not static, it is merely the
|
||||
// unchangeable background for the mod. Changes to the mod are kept in a separate layer, which can be loaded and saved.
|
||||
type Mod struct {
|
||||
worldManifest *Manifest
|
||||
resourcesChanged resource.ModificationCallback
|
||||
resetCallback ModResetCallback
|
||||
|
||||
lastChangeTime time.Time
|
||||
changedFiles map[string]struct{}
|
||||
|
||||
data ModData
|
||||
}
|
||||
|
||||
// NewMod returns a new instance.
|
||||
func NewMod(resourcesChanged resource.ModificationCallback, resetCallback ModResetCallback) *Mod {
|
||||
mod := &Mod{
|
||||
resourcesChanged: resourcesChanged,
|
||||
resetCallback: resetCallback,
|
||||
changedFiles: make(map[string]struct{}),
|
||||
}
|
||||
mod.worldManifest = NewManifest(mod.worldChanged)
|
||||
mod.data.FileChangeCallback = mod.markFileChanged
|
||||
|
||||
return mod
|
||||
}
|
||||
|
||||
// World returns the static background to the mod. Changes in the returned manifest may cause change callbacks
|
||||
// being forwarded.
|
||||
func (mod Mod) World() *Manifest {
|
||||
return mod.worldManifest
|
||||
}
|
||||
|
||||
// ModifiedResources returns the current modification state.
|
||||
func (mod Mod) ModifiedResources() []*LocalizedResources {
|
||||
return mod.data.LocalizedResources
|
||||
}
|
||||
|
||||
// ModifiedFilenames returns the list of all filenames suspected of change.
|
||||
func (mod Mod) ModifiedFilenames() []string {
|
||||
result := make([]string, 0, len(mod.changedFiles))
|
||||
for filename := range mod.changedFiles {
|
||||
result = append(result, filename)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// AllAbsoluteFilenames returns the list of all filenames currently loaded in the mod.
|
||||
func (mod Mod) AllAbsoluteFilenames(reference string) []string {
|
||||
var result []string
|
||||
for _, res := range mod.data.LocalizedResources {
|
||||
result = append(result, res.File.AbsolutePathFrom(reference))
|
||||
}
|
||||
if len(mod.data.ObjectProperties) > 0 {
|
||||
result = append(result, filepath.Join(reference, ObjectPropertiesFilename))
|
||||
}
|
||||
if len(mod.data.TextureProperties) > 0 {
|
||||
result = append(result, filepath.Join(reference, TexturePropertiesFilename))
|
||||
}
|
||||
sort.Strings(result)
|
||||
return result
|
||||
}
|
||||
|
||||
// LastChangeTime returns the timestamp of the last change. Zero if not modified.
|
||||
func (mod *Mod) LastChangeTime() time.Time {
|
||||
return mod.lastChangeTime
|
||||
}
|
||||
|
||||
// ResetLastChangeTime clears the last change timestamp. It will be set again at the next modification.
|
||||
func (mod *Mod) ResetLastChangeTime() {
|
||||
mod.lastChangeTime = time.Time{}
|
||||
}
|
||||
|
||||
// MarkSave clears the list of modified filenames.
|
||||
func (mod *Mod) MarkSave() {
|
||||
mod.changedFiles = make(map[string]struct{})
|
||||
mod.lastChangeTime = time.Time{}
|
||||
}
|
||||
|
||||
// ModifiedResource retrieves the resource of given language and ID.
|
||||
// There is no fallback lookup, it will return the exact resource stored under the provided identifier.
|
||||
// Returns nil if the resource does not exist.
|
||||
func (mod Mod) ModifiedResource(lang resource.Language, id resource.ID) resource.View {
|
||||
return mod.modifiedResource(lang, id)
|
||||
}
|
||||
|
||||
func (mod Mod) modifiedResource(lang resource.Language, id resource.ID) *resource.Resource {
|
||||
for _, entry := range mod.data.LocalizedResources {
|
||||
if entry.Language == lang {
|
||||
res, err := entry.Store.Resource(id)
|
||||
if err == nil {
|
||||
return res
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateBlockPatch creates delta information for a block witch static data length.
|
||||
// The returned patch structure contains details for both modifying the current state to be equal the new state,
|
||||
// as well as the reversal delta. These deltas are calculated using the rle compression package.
|
||||
// The returned boolean indicates whether the data differs. This can be used to detect whether the patch is necessary.
|
||||
// An error is returned if the resource or the block do not exist, or if the length of newData does not match that of the block.
|
||||
//
|
||||
// If no error is returned, both the patch and the boolean provide valid information - even if the data is equal.
|
||||
func (mod Mod) CreateBlockPatch(lang resource.Language, id resource.ID, index int, newData []byte) (BlockPatch, bool, error) {
|
||||
patch := BlockPatch{
|
||||
ID: id,
|
||||
BlockIndex: -1,
|
||||
BlockLength: 0,
|
||||
}
|
||||
res := mod.modifiedResource(lang, id)
|
||||
if res == nil {
|
||||
return patch, false, resource.ErrNotFound(id)
|
||||
}
|
||||
oldData, err := res.BlockRaw(index)
|
||||
if err != nil {
|
||||
return patch, false, err
|
||||
}
|
||||
if len(oldData) != len(newData) {
|
||||
return patch, false, errBlockLengthMismatch
|
||||
}
|
||||
|
||||
forwardData := bytes.NewBuffer(nil)
|
||||
err = rle.Compress(forwardData, newData, oldData)
|
||||
if err != nil {
|
||||
return patch, false, err
|
||||
}
|
||||
patch.ForwardData = forwardData.Bytes()
|
||||
|
||||
reverseData := bytes.NewBuffer(nil)
|
||||
err = rle.Compress(reverseData, oldData, newData)
|
||||
if err != nil {
|
||||
return patch, false, err
|
||||
}
|
||||
patch.ReverseData = reverseData.Bytes()
|
||||
|
||||
patch.BlockIndex = index
|
||||
patch.BlockLength = len(oldData)
|
||||
|
||||
return patch, !bytes.Equal(oldData, newData), nil
|
||||
}
|
||||
|
||||
// ModifiedBlock retrieves the specific block identified by given parameter.
|
||||
// Returns empty slice if the block (or resource) is not modified.
|
||||
func (mod Mod) ModifiedBlock(lang resource.Language, id resource.ID, index int) (data []byte) {
|
||||
res := mod.modifiedResource(lang, id)
|
||||
if res == nil {
|
||||
return
|
||||
}
|
||||
raw, _ := res.BlockRaw(index)
|
||||
return mod.blockCopy(raw)
|
||||
}
|
||||
|
||||
// ModifiedBlocks returns all blocks of the modified resource.
|
||||
func (mod Mod) ModifiedBlocks(lang resource.Language, id resource.ID) [][]byte {
|
||||
res := mod.modifiedResource(lang, id)
|
||||
if res == nil {
|
||||
return nil
|
||||
}
|
||||
data := make([][]byte, res.BlockCount())
|
||||
for index := 0; index < res.BlockCount(); index++ {
|
||||
raw, _ := res.BlockRaw(index)
|
||||
data[index] = mod.blockCopy(raw)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func (mod Mod) blockCopy(data []byte) []byte {
|
||||
result := make([]byte, len(data))
|
||||
copy(result, data)
|
||||
return result
|
||||
}
|
||||
|
||||
// Filter returns a list of resources that match the given parameters.
|
||||
func (mod Mod) Filter(lang resource.Language, id resource.ID) resource.List {
|
||||
list := mod.worldManifest.Filter(lang, id)
|
||||
if res := mod.modifiedResource(resource.LangAny, id); res != nil {
|
||||
list = list.With(res)
|
||||
}
|
||||
for _, worldLang := range resource.Languages() {
|
||||
if worldLang.Includes(lang) {
|
||||
if res := mod.modifiedResource(lang, id); res != nil {
|
||||
list = list.With(res)
|
||||
}
|
||||
}
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
// LocalizedResources returns a resource selector for a specific language.
|
||||
func (mod Mod) LocalizedResources(lang resource.Language) resource.Selector {
|
||||
return resource.Selector{
|
||||
Lang: lang,
|
||||
From: mod,
|
||||
As: ResourceViewStrategy(),
|
||||
}
|
||||
}
|
||||
|
||||
// Modify requests to change the mod. The provided function will be called to collect all changes.
|
||||
// After the modifier completes, all the requests will be applied and any changes notified.
|
||||
func (mod *Mod) Modify(modifier func(Modder)) {
|
||||
var trans ModTransaction
|
||||
modifier(&trans)
|
||||
mod.modifyAndNotify(func() {
|
||||
for _, action := range trans.actions {
|
||||
action(&mod.data)
|
||||
}
|
||||
}, trans.modifiedIDs.ToList())
|
||||
}
|
||||
|
||||
// ObjectProperties returns the table of object properties.
|
||||
func (mod *Mod) ObjectProperties() object.PropertiesTable {
|
||||
if mod.HasModifiableObjectProperties() {
|
||||
return mod.data.ObjectProperties
|
||||
}
|
||||
return mod.worldManifest.ObjectProperties()
|
||||
}
|
||||
|
||||
// HasModifiableObjectProperties returns true if the mod has dedicated object properties.
|
||||
func (mod *Mod) HasModifiableObjectProperties() bool {
|
||||
return len(mod.data.ObjectProperties) > 0
|
||||
}
|
||||
|
||||
// TextureProperties returns the list of texture properties.
|
||||
func (mod *Mod) TextureProperties() texture.PropertiesList {
|
||||
if mod.HasModifiableTextureProperties() {
|
||||
return mod.data.TextureProperties
|
||||
}
|
||||
return mod.worldManifest.TextureProperties()
|
||||
}
|
||||
|
||||
// HasModifiableTextureProperties returns true if the mod has dedicated texture properties.
|
||||
func (mod *Mod) HasModifiableTextureProperties() bool {
|
||||
return len(mod.data.TextureProperties) > 0
|
||||
}
|
||||
|
||||
func (mod *Mod) modifyAndNotify(modifier func(), modifiedIDs []resource.ID) {
|
||||
notifier := resource.ChangeNotifier{
|
||||
Callback: mod.resourcesChanged,
|
||||
Localizer: mod,
|
||||
}
|
||||
notifier.ModifyAndNotify(modifier, modifiedIDs)
|
||||
}
|
||||
|
||||
func (mod Mod) worldChanged(modifiedIDs []resource.ID, failedIDs []resource.ID) {
|
||||
// It would be great to also check whether the mod hides any of these changes.
|
||||
// Sadly, this is not possible:
|
||||
// a) At the point of this callback, we can't do a check on the previous state anymore.
|
||||
// b) Even when changing the world only within a modification enclosure of our own notifier, we can't determine
|
||||
// the list of changed IDs before actually changing them. (Specifying ALL IDs is not a good idea due to performance.)
|
||||
// As a result, simply forward this list. I don't even expect any big performance gain through such a filter.
|
||||
// This would only be relevant to "full conversion" mods AND a change in a big list in the world. Hardly the case.
|
||||
mod.resourcesChanged(modifiedIDs, failedIDs)
|
||||
}
|
||||
|
||||
// Reset changes the mod to a new set of resources.
|
||||
func (mod *Mod) Reset(newResources []*LocalizedResources, objectProperties object.PropertiesTable, textureProperties texture.PropertiesList) {
|
||||
var modifiedIDs resource.IDMarkerMap
|
||||
collectIDs := func(res []*LocalizedResources) {
|
||||
for _, loc := range res {
|
||||
for _, id := range loc.Store.IDs() {
|
||||
modifiedIDs.Add(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
collectIDs(mod.data.LocalizedResources)
|
||||
collectIDs(newResources)
|
||||
|
||||
mod.data.LocalizedResources = newResources
|
||||
mod.data.ObjectProperties = objectProperties
|
||||
mod.data.TextureProperties = textureProperties
|
||||
mod.changedFiles = make(map[string]struct{})
|
||||
mod.lastChangeTime = time.Time{}
|
||||
mod.resetCallback()
|
||||
mod.resourcesChanged(modifiedIDs.ToList(), nil)
|
||||
}
|
||||
|
||||
func (mod *Mod) markFileChanged(filename string) {
|
||||
mod.changedFiles[filename] = struct{}{}
|
||||
mod.lastChangeTime = time.Now()
|
||||
}
|
||||
|
||||
// FixListResources ensures all resources that contain resource lists to
|
||||
// have maximum size. This is done to ensure compatibility with layered modding in the
|
||||
// Source Port branch of engines.
|
||||
// These engines will have "lower" mods bleed through only if the block is empty in a
|
||||
// "higher" mod. This even counts for entries past the last modified one in the higher mod.
|
||||
func (mod *Mod) FixListResources() {
|
||||
mod.Modify(func(modder Modder) {
|
||||
for _, localized := range mod.data.LocalizedResources {
|
||||
for _, id := range localized.Store.IDs() {
|
||||
res, _ := localized.Store.Resource(id)
|
||||
info, known := ids.Info(id)
|
||||
if known && info.List {
|
||||
baseCount := res.BlockCount()
|
||||
required := info.MaxCount - baseCount
|
||||
for i := 0; i < required; i++ {
|
||||
modder.SetResourceBlock(localized.Language, id, baseCount+i, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
package world
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/content/object"
|
||||
"github.com/inkyblackness/hacked/ss1/content/texture"
|
||||
"github.com/inkyblackness/hacked/ss1/resource"
|
||||
"github.com/inkyblackness/hacked/ss1/serial/rle"
|
||||
"github.com/inkyblackness/hacked/ss1/world/ids"
|
||||
)
|
||||
|
||||
// LocalizedResources associates a language with a resource store under a specific filename.
|
||||
type LocalizedResources struct {
|
||||
// File describes from which file the data was possibly loaded from, and where it should be stored in.
|
||||
File FileLocation
|
||||
// Template specifies the "original" filename the data would be stored in.
|
||||
// For example, savegames have "archive.dat" as their template.
|
||||
// Note: This could be expanded to be a list. The loader could check which IDs are all stored in the loaded file,
|
||||
// and match all corresponding templates. Though, this works only for "Any" language resources.
|
||||
Template string
|
||||
// Language identifies the language this package of resources is for.
|
||||
Language resource.Language
|
||||
// Store contains all the resources.
|
||||
Store resource.Store
|
||||
}
|
||||
|
||||
// ModData contains the core information about a mod.
|
||||
type ModData struct {
|
||||
FileChangeCallback func(string)
|
||||
|
||||
LocalizedResources []*LocalizedResources
|
||||
ObjectProperties object.PropertiesTable
|
||||
TextureProperties texture.PropertiesList
|
||||
}
|
||||
|
||||
// SetResourceBlock changes the block data of a resource.
|
||||
//
|
||||
// If the block data is not empty, then:
|
||||
// If the resource does not exist, it will be created with default meta information.
|
||||
// If the block does not exist, the resource is extended to allow its addition.
|
||||
//
|
||||
// If the block data is empty (or nil), then the block is cleared.
|
||||
func (data *ModData) SetResourceBlock(lang resource.Language, id resource.ID, index int, blockData []byte) {
|
||||
loc, res := data.ensureResource(lang, id)
|
||||
res.SetBlock(index, blockData)
|
||||
data.notifyFileChanged(loc.File.Name)
|
||||
}
|
||||
|
||||
// PatchResourceBlock modifies an existing block.
|
||||
// This modification assumes the block already exists and can take the given patch data.
|
||||
// The patch data is expected to be produced by rle.Compress().
|
||||
func (data *ModData) PatchResourceBlock(lang resource.Language, id resource.ID, index int, expectedLength int, patch []byte) {
|
||||
loc, res := data.ensureResource(lang, id)
|
||||
raw, err := res.BlockRaw(index)
|
||||
if (err == nil) && (len(raw) == expectedLength) {
|
||||
_ = rle.Decompress(bytes.NewReader(patch), raw)
|
||||
data.notifyFileChanged(loc.File.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// SetResourceBlocks sets the entire list of block data of a resource.
|
||||
// This method is primarily meant for compound non-list resources (e.g. text pages).
|
||||
func (data *ModData) SetResourceBlocks(lang resource.Language, id resource.ID, blocks [][]byte) {
|
||||
loc, res := data.ensureResource(lang, id)
|
||||
res.Set(blocks)
|
||||
data.notifyFileChanged(loc.File.Name)
|
||||
}
|
||||
|
||||
// DelResource removes a resource from the mod in the given language.
|
||||
func (data *ModData) DelResource(lang resource.Language, id resource.ID) {
|
||||
for _, loc := range data.LocalizedResources {
|
||||
if (loc.Language == lang) && loc.Store.Del(id) {
|
||||
data.notifyFileChanged(loc.File.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SetTextureProperties updates the properties of a specific texture.
|
||||
func (data *ModData) SetTextureProperties(index int, properties texture.Properties) {
|
||||
if (index >= 0) && (index < len(data.TextureProperties)) {
|
||||
data.TextureProperties[index] = properties
|
||||
data.notifyFileChanged(TexturePropertiesFilename)
|
||||
}
|
||||
}
|
||||
|
||||
// SetObjectProperties updates the properties of a specific object.
|
||||
func (data *ModData) SetObjectProperties(triple object.Triple, properties object.Properties) {
|
||||
entry, err := data.ObjectProperties.ForObject(triple)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
*entry = properties.Clone()
|
||||
data.notifyFileChanged(ObjectPropertiesFilename)
|
||||
}
|
||||
|
||||
func (data *ModData) ensureResource(lang resource.Language, id resource.ID) (*LocalizedResources, *resource.Resource) {
|
||||
for _, loc := range data.LocalizedResources {
|
||||
if loc.Language == lang {
|
||||
res, err := loc.Store.Resource(id)
|
||||
if err == nil {
|
||||
return loc, res
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return data.newResource(lang, id)
|
||||
}
|
||||
|
||||
func (data *ModData) newResource(lang resource.Language, id resource.ID) (*LocalizedResources, *resource.Resource) {
|
||||
compound := true
|
||||
contentType := resource.ContentType(0xFF) // Default to something completely unknown.
|
||||
compressed := false
|
||||
filename := "unknown.res"
|
||||
|
||||
if info, known := ids.Info(id); known {
|
||||
compound = info.Compound
|
||||
contentType = info.ContentType
|
||||
compressed = info.Compressed
|
||||
filename = info.ResFile.For(lang)
|
||||
}
|
||||
|
||||
loc := data.ensureStore(lang, filename)
|
||||
_ = loc.Store.Put(id, resource.Resource{
|
||||
Properties: resource.Properties{
|
||||
Compound: compound,
|
||||
ContentType: contentType,
|
||||
Compressed: compressed,
|
||||
},
|
||||
})
|
||||
res, _ := loc.Store.Resource(id)
|
||||
return loc, res
|
||||
}
|
||||
|
||||
func (data *ModData) ensureStore(lang resource.Language, filename string) *LocalizedResources {
|
||||
for _, loc := range data.LocalizedResources {
|
||||
if (loc.Language == lang) && (loc.Template == filename) {
|
||||
return loc
|
||||
}
|
||||
}
|
||||
loc := &LocalizedResources{
|
||||
File: FileLocation{DirPath: ".", Name: filename},
|
||||
Template: filename,
|
||||
Language: lang,
|
||||
}
|
||||
data.LocalizedResources = append(data.LocalizedResources, loc)
|
||||
return loc
|
||||
}
|
||||
|
||||
func (data ModData) notifyFileChanged(filename string) {
|
||||
if data.FileChangeCallback != nil {
|
||||
data.FileChangeCallback(filename)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package world
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/hacked/ss1/content/object"
|
||||
"github.com/inkyblackness/hacked/ss1/content/texture"
|
||||
"github.com/inkyblackness/hacked/ss1/resource"
|
||||
)
|
||||
|
||||
type modAction func(modder Modder)
|
||||
|
||||
// ModTransaction is used to queue a list of modifications.
|
||||
// It allows modifications of related resources in one atomic action.
|
||||
type ModTransaction struct {
|
||||
actions []modAction
|
||||
modifiedIDs resource.IDMarkerMap
|
||||
}
|
||||
|
||||
// SetResourceBlock changes the block data of a resource.
|
||||
//
|
||||
// If the block data is not empty, then:
|
||||
// If the resource does not exist, it will be created with default meta information.
|
||||
// If the block does not exist, the resource is extended to allow its addition.
|
||||
//
|
||||
// If the block data is empty (or nil), then the block is cleared.
|
||||
// If the resource is a compound list, then the underlying data will become visible again.
|
||||
func (trans *ModTransaction) SetResourceBlock(lang resource.Language, id resource.ID, index int, data []byte) {
|
||||
trans.actions = append(trans.actions, func(modder Modder) {
|
||||
modder.SetResourceBlock(lang, id, index, data)
|
||||
})
|
||||
trans.modifiedIDs.Add(id)
|
||||
}
|
||||
|
||||
// PatchResourceBlock modifies an existing block.
|
||||
// This modification assumes the block already exists and can take the given patch data.
|
||||
// The patch data is expected to be produced by rle.Compress().
|
||||
func (trans *ModTransaction) PatchResourceBlock(lang resource.Language, id resource.ID, index int, expectedLength int, patch []byte) {
|
||||
trans.actions = append(trans.actions, func(modder Modder) {
|
||||
modder.PatchResourceBlock(lang, id, index, expectedLength, patch)
|
||||
})
|
||||
trans.modifiedIDs.Add(id)
|
||||
}
|
||||
|
||||
// SetResourceBlocks sets the entire list of block data of a resource.
|
||||
// This method is primarily meant for compound non-list resources (e.g. text pages).
|
||||
func (trans *ModTransaction) SetResourceBlocks(lang resource.Language, id resource.ID, data [][]byte) {
|
||||
trans.actions = append(trans.actions, func(modder Modder) {
|
||||
modder.SetResourceBlocks(lang, id, data)
|
||||
})
|
||||
trans.modifiedIDs.Add(id)
|
||||
}
|
||||
|
||||
// DelResource removes a resource from the mod in the given language.
|
||||
//
|
||||
// After the deletion, all the underlying data of the world will become visible again.
|
||||
func (trans *ModTransaction) DelResource(lang resource.Language, id resource.ID) {
|
||||
trans.actions = append(trans.actions, func(modder Modder) {
|
||||
modder.DelResource(lang, id)
|
||||
})
|
||||
trans.modifiedIDs.Add(id)
|
||||
}
|
||||
|
||||
// SetTextureProperties updates the properties of a specific texture.
|
||||
func (trans *ModTransaction) SetTextureProperties(textureIndex int, properties texture.Properties) {
|
||||
trans.actions = append(trans.actions, func(modder Modder) {
|
||||
modder.SetTextureProperties(textureIndex, properties)
|
||||
})
|
||||
}
|
||||
|
||||
// SetObjectProperties updates the properties of a specific object.
|
||||
func (trans *ModTransaction) SetObjectProperties(triple object.Triple, properties object.Properties) {
|
||||
trans.actions = append(trans.actions, func(modder Modder) {
|
||||
modder.SetObjectProperties(triple, properties)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
package world_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/resource"
|
||||
"github.com/inkyblackness/hacked/ss1/world"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type ModSuite struct {
|
||||
suite.Suite
|
||||
mod *world.Mod
|
||||
|
||||
selector *resource.Selector
|
||||
|
||||
lastModifiedIDs []resource.ID
|
||||
lastFailedIDs []resource.ID
|
||||
}
|
||||
|
||||
func TestModSuite(t *testing.T) {
|
||||
suite.Run(t, new(ModSuite))
|
||||
}
|
||||
|
||||
func (suite *ModSuite) SetupTest() {
|
||||
suite.mod = world.NewMod(suite.onResourcesModified, func() {})
|
||||
|
||||
suite.selector = nil
|
||||
|
||||
suite.lastModifiedIDs = nil
|
||||
suite.lastFailedIDs = nil
|
||||
}
|
||||
|
||||
func (suite *ModSuite) onResourcesModified(modifiedIDs []resource.ID, failedIDs []resource.ID) {
|
||||
suite.lastModifiedIDs = modifiedIDs
|
||||
suite.lastFailedIDs = failedIDs
|
||||
}
|
||||
|
||||
func (suite *ModSuite) TestResourcesCanBeRetrievedFromTheWorld() {
|
||||
suite.givenWorldHas(
|
||||
suite.someLocalizedResources(resource.LangAny,
|
||||
suite.storing(0x0800, [][]byte{{0xAA}})))
|
||||
suite.whenResourcesAreQueriedFor(resource.LangAny)
|
||||
suite.thenResourcesCanBeSelected(0x0800)
|
||||
}
|
||||
|
||||
func (suite *ModSuite) TestResourcesCanBeModified() {
|
||||
suite.givenWorldHas(
|
||||
suite.someLocalizedResources(resource.LangAny,
|
||||
suite.storing(0x0800, [][]byte{{0xAA}})))
|
||||
suite.whenModifyingBy(func(modder world.Modder) {
|
||||
modder.SetResourceBlock(resource.LangAny, 0x0800, 0, []byte{0xBB})
|
||||
})
|
||||
suite.thenResourceBlockShouldBe(resource.LangAny, 0x0800, 0, []byte{0xBB})
|
||||
}
|
||||
|
||||
func (suite *ModSuite) TestResourcesCanBeExtended() {
|
||||
suite.givenWorldHas(
|
||||
suite.someLocalizedResources(resource.LangAny,
|
||||
suite.storing(0x0800, [][]byte{{0xAA}})))
|
||||
suite.whenModifyingBy(func(modder world.Modder) {
|
||||
modder.SetResourceBlock(resource.LangAny, 0x0800, 2, []byte{0xBB})
|
||||
})
|
||||
suite.thenResourceBlockShouldBe(resource.LangAny, 0x0800, 2, []byte{0xBB})
|
||||
}
|
||||
|
||||
func (suite *ModSuite) TestResourcesCanBeRemoved() {
|
||||
suite.givenWorldHas(
|
||||
suite.someLocalizedResources(resource.LangAny,
|
||||
suite.storing(0x0800, [][]byte{{0xAA}})))
|
||||
suite.givenModifiedBy(func(modder world.Modder) {
|
||||
modder.SetResourceBlock(resource.LangAny, 0x0800, 0, []byte{0xBB})
|
||||
})
|
||||
suite.whenModifyingBy(func(modder world.Modder) {
|
||||
modder.DelResource(resource.LangAny, 0x0800)
|
||||
})
|
||||
suite.thenResourceBlockShouldBe(resource.LangAny, 0x0800, 0, []byte{0xAA})
|
||||
}
|
||||
|
||||
func (suite *ModSuite) TestAdditionsAreNotified() {
|
||||
suite.givenWorldHas(
|
||||
suite.someLocalizedResources(resource.LangAny,
|
||||
suite.storing(0x0800, [][]byte{{0xAA}})))
|
||||
suite.whenModifyingBy(func(modder world.Modder) {
|
||||
modder.SetResourceBlock(resource.LangAny, 0x0800, 2, []byte{0xBB})
|
||||
})
|
||||
suite.thenModifiedResourcesShouldBe(0x0800)
|
||||
}
|
||||
|
||||
func (suite *ModSuite) TestModificationsToIdenticalDataAreNotNotified() {
|
||||
suite.givenWorldHas(
|
||||
suite.someLocalizedResources(resource.LangAny,
|
||||
suite.storing(0x004C, [][]byte{{0xAA}})))
|
||||
suite.whenModifyingBy(func(modder world.Modder) {
|
||||
modder.SetResourceBlock(resource.LangAny, 0x004C, 0, []byte{0xAA})
|
||||
})
|
||||
suite.thenModifiedResourcesShouldBe()
|
||||
}
|
||||
|
||||
func (suite *ModSuite) TestDeletionIsNotified() {
|
||||
suite.givenWorldHas(
|
||||
suite.someLocalizedResources(resource.LangAny,
|
||||
suite.storing(0x0800, [][]byte{{0xAA}})))
|
||||
suite.givenModifiedBy(func(modder world.Modder) {
|
||||
modder.SetResourceBlock(resource.LangAny, 0x0800, 0, []byte{0xBB})
|
||||
})
|
||||
suite.whenModifyingBy(func(modder world.Modder) {
|
||||
modder.DelResource(resource.LangAny, 0x0800)
|
||||
})
|
||||
suite.thenModifiedResourcesShouldBe(0x0800)
|
||||
}
|
||||
|
||||
func (suite *ModSuite) TestModifiedResourceCanBeRetrieved() {
|
||||
suite.whenModifyingBy(func(modder world.Modder) {
|
||||
modder.SetResourceBlock(resource.LangAny, 0x0800, 0, []byte{0xBB})
|
||||
modder.SetResourceBlock(resource.LangAny, 0x0800, 1, []byte{0xCC})
|
||||
})
|
||||
|
||||
res := suite.mod.ModifiedResource(resource.LangAny, 0x0800)
|
||||
require.NotNil(suite.T(), res, "Resource expected")
|
||||
assert.Equal(suite.T(), 2, res.BlockCount(), "Two blocks expected")
|
||||
}
|
||||
|
||||
func (suite *ModSuite) TestModifiedBlocksCanBeRetrievedSingle() {
|
||||
suite.whenModifyingBy(func(modder world.Modder) {
|
||||
modder.SetResourceBlock(resource.LangAny, 0x0800, 0, []byte{0xBB})
|
||||
modder.SetResourceBlock(resource.LangAny, 0x0800, 1, []byte{0xCC})
|
||||
})
|
||||
|
||||
assert.Equal(suite.T(), []byte{0xBB}, suite.mod.ModifiedBlock(resource.LangAny, 0x0800, 0))
|
||||
assert.Equal(suite.T(), []byte{0xCC}, suite.mod.ModifiedBlock(resource.LangAny, 0x0800, 1))
|
||||
}
|
||||
|
||||
func (suite *ModSuite) TestModifiedBlocksCanBeRetrievedBulk() {
|
||||
suite.whenModifyingBy(func(modder world.Modder) {
|
||||
modder.SetResourceBlock(resource.LangAny, 0x0800, 0, []byte{0xBB})
|
||||
modder.SetResourceBlock(resource.LangAny, 0x0800, 1, []byte{0xCC})
|
||||
})
|
||||
|
||||
assert.Equal(suite.T(), [][]byte{{0xBB}, {0xCC}}, suite.mod.ModifiedBlocks(resource.LangAny, 0x0800))
|
||||
}
|
||||
|
||||
func (suite *ModSuite) givenWorldHas(res ...resource.LocalizedResources) {
|
||||
suite.whenWorldIsExtendedWith(res...)
|
||||
suite.lastModifiedIDs = nil
|
||||
suite.lastFailedIDs = nil
|
||||
}
|
||||
|
||||
func (suite *ModSuite) whenWorldIsExtendedWith(res ...resource.LocalizedResources) {
|
||||
manifest := suite.mod.World()
|
||||
at := manifest.EntryCount()
|
||||
id := fmt.Sprintf("entry-%d", at)
|
||||
err := suite.mod.World().InsertEntry(at, suite.anEntryWithResources(id, res...))
|
||||
require.Nil(suite.T(), err, fmt.Sprintf("No error expected inserting entry at %d", at))
|
||||
}
|
||||
|
||||
func (suite *ModSuite) whenResourcesAreQueriedFor(lang resource.Language) {
|
||||
selector := suite.mod.LocalizedResources(lang)
|
||||
suite.selector = &selector
|
||||
}
|
||||
|
||||
func (suite *ModSuite) givenModifiedBy(modifier func(world.Modder)) {
|
||||
suite.mod.Modify(modifier)
|
||||
|
||||
suite.lastModifiedIDs = nil
|
||||
suite.lastFailedIDs = nil
|
||||
}
|
||||
|
||||
func (suite *ModSuite) whenModifyingBy(modifier func(world.Modder)) {
|
||||
suite.mod.Modify(modifier)
|
||||
}
|
||||
|
||||
func (suite *ModSuite) thenResourceBlockShouldBe(lang resource.Language, id int, blockIndex int, expected []byte) {
|
||||
view, viewErr := suite.mod.LocalizedResources(lang).Select(resource.ID(id))
|
||||
require.Nil(suite.T(), viewErr, "No error expected selecting resource")
|
||||
reader, blockErr := view.Block(blockIndex)
|
||||
require.Nil(suite.T(), blockErr, "No error expected retrieving block")
|
||||
data, dataErr := ioutil.ReadAll(reader)
|
||||
require.Nil(suite.T(), dataErr, "No error expected reading data")
|
||||
assert.Equal(suite.T(), expected, data, "Data mismatch in block")
|
||||
}
|
||||
|
||||
func (suite *ModSuite) thenResourcesCanBeSelected(id int) {
|
||||
view, err := suite.selector.Select(resource.ID(id))
|
||||
assert.Nil(suite.T(), err, "No error expected")
|
||||
assert.NotNil(suite.T(), view, "View expected")
|
||||
}
|
||||
|
||||
func (suite *ModSuite) thenModifiedResourcesShouldBe(expected ...int) {
|
||||
identified := make([]resource.ID, len(expected))
|
||||
for index, id := range expected {
|
||||
identified[index] = resource.ID(id)
|
||||
}
|
||||
suite.sortIDs(identified)
|
||||
suite.sortIDs(suite.lastModifiedIDs)
|
||||
|
||||
assert.Equal(suite.T(), identified, suite.lastModifiedIDs, "Modified IDs don't match")
|
||||
}
|
||||
|
||||
func (suite *ModSuite) sortIDs(ids []resource.ID) {
|
||||
sort.Slice(ids, func(a, b int) bool { return ids[a] < ids[b] })
|
||||
}
|
||||
|
||||
func (suite *ModSuite) someLocalizedResources(lang resource.Language, modifiers ...func(*resource.Store)) resource.LocalizedResources {
|
||||
var store resource.Store
|
||||
for _, modifier := range modifiers {
|
||||
modifier(&store)
|
||||
}
|
||||
return resource.LocalizedResources{
|
||||
ID: "unnamed",
|
||||
Language: lang,
|
||||
Viewer: store,
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *ModSuite) anEntryWithResources(id string, res ...resource.LocalizedResources) *world.ManifestEntry {
|
||||
return &world.ManifestEntry{
|
||||
ID: id,
|
||||
Resources: res,
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *ModSuite) storing(id int, data [][]byte) func(*resource.Store) {
|
||||
return func(store *resource.Store) {
|
||||
_ = store.Put(resource.ID(id), resource.Resource{
|
||||
Blocks: resource.BlocksFrom(data),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package world
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/hacked/ss1/content/object"
|
||||
"github.com/inkyblackness/hacked/ss1/content/texture"
|
||||
"github.com/inkyblackness/hacked/ss1/resource"
|
||||
)
|
||||
|
||||
// Modder describes actions meant to modify resources.
|
||||
type Modder interface {
|
||||
|
||||
// SetResourceBlock changes the block data of a resource.
|
||||
//
|
||||
// If the block data is not empty, then:
|
||||
// If the resource does not exist, it will be created with default meta information.
|
||||
// If the block does not exist, the resource is extended to allow its addition.
|
||||
//
|
||||
// If the block data is empty (or nil), then the block is cleared.
|
||||
// If the resource is a compound list, then the underlying data will become visible again.
|
||||
SetResourceBlock(lang resource.Language, id resource.ID, index int, data []byte)
|
||||
|
||||
// PatchResourceBlock modifies an existing block.
|
||||
// This modification assumes the block already exists and can take the given patch data.
|
||||
// The patch data is expected to be produced by rle.Compress(). (see also: Mod.CreateBlockPatch)
|
||||
PatchResourceBlock(lang resource.Language, id resource.ID, index int, expectedLength int, patch []byte)
|
||||
|
||||
// SetResourceBlocks sets the entire list of block data of a resource.
|
||||
// This method is primarily meant for compound non-list resources (e.g. text pages).
|
||||
SetResourceBlocks(lang resource.Language, id resource.ID, data [][]byte)
|
||||
|
||||
// DelResource removes a resource from the mod in the given language.
|
||||
//
|
||||
// After the deletion, all the underlying data of the world will become visible again.
|
||||
DelResource(lang resource.Language, id resource.ID)
|
||||
|
||||
// SetTextureProperties updates the properties of a specific texture.
|
||||
SetTextureProperties(textureIndex int, properties texture.Properties)
|
||||
|
||||
// SetObjectProperties updates the properties of a specific object.
|
||||
SetObjectProperties(triple object.Triple, properties object.Properties)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package world
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/hacked/ss1/resource"
|
||||
"github.com/inkyblackness/hacked/ss1/world/ids"
|
||||
)
|
||||
|
||||
// ResourceViewStrategy returns a strategy that is typical for the game.
|
||||
func ResourceViewStrategy() resource.ViewStrategy {
|
||||
return defaultResources{}
|
||||
}
|
||||
|
||||
type defaultResources struct{}
|
||||
|
||||
func (def defaultResources) IsCompoundList(id resource.ID) bool {
|
||||
info, _ := ids.Info(id)
|
||||
return info.List
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package citadel
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/hacked/ss1/content/archive"
|
||||
"github.com/inkyblackness/hacked/ss1/world"
|
||||
)
|
||||
|
||||
// DefaultGameState returns a new instance of a game state that the engine creates for a standard game.
|
||||
func DefaultGameState() *archive.GameState {
|
||||
state := archive.DefaultGameState()
|
||||
|
||||
// location: medical level
|
||||
state.Set("Current Level", world.StartingLevel)
|
||||
|
||||
// location: in the neurosurgery chamber, looking West
|
||||
state.Set("Hacker Position X", (world.StartingTileX<<16)+0x8000)
|
||||
state.Set("Hacker Position Y", (world.StartingTileY<<16)+0x8000)
|
||||
state.Set("Hacker Position Z", 0x01BD00)
|
||||
state.Set("Hacker Yaw", 0x03243E)
|
||||
|
||||
// set first message
|
||||
state.EMailState(26).SetReceived(true) // Rebecca Lansing's first message
|
||||
// The engine also sets the active state of email, though this field index is not used.
|
||||
|
||||
for index, info := range booleanVariables {
|
||||
if info.InitValue == nil {
|
||||
continue
|
||||
}
|
||||
state.SetBooleanVar(index, *info.InitValue != 0)
|
||||
}
|
||||
for index, info := range integerVariables {
|
||||
if info.InitValue == nil {
|
||||
continue
|
||||
}
|
||||
state.SetIntegerVar(index, *info.InitValue)
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
package citadel
|
||||
|
||||
import "github.com/inkyblackness/hacked/ss1/content/archive"
|
||||
|
||||
var integerVariables = archive.GameVariables{
|
||||
3: archive.GameVariableInfoFor("Engine State").At(2).LimitedBy(0, 100),
|
||||
|
||||
12: archive.GameVariableInfoFor("Number of Groves").At(3).LimitedBy(0, 4),
|
||||
|
||||
33: archive.GameVariableInfoFor("Destroyed CPUs Level 1").At(0).LimitedBy(0, 100),
|
||||
34: archive.GameVariableInfoFor("Destroyed CPUs Level 2").At(0).LimitedBy(0, 100),
|
||||
35: archive.GameVariableInfoFor("Destroyed CPUs Level 3").At(0).LimitedBy(0, 100),
|
||||
36: archive.GameVariableInfoFor("Destroyed CPUs Level 4").At(0).LimitedBy(0, 100),
|
||||
37: archive.GameVariableInfoFor("Destroyed CPUs Level 5").At(0).LimitedBy(0, 100),
|
||||
38: archive.GameVariableInfoFor("Destroyed CPUs Level 6").At(0).LimitedBy(0, 100),
|
||||
|
||||
39: archive.GameVariableInfoFor("Killed Cyberguards, largce CSpace").At(0).LimitedBy(0, 100),
|
||||
}
|
||||
|
||||
var booleanVariables = archive.GameVariables{
|
||||
1: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
2: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
3: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
16: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
18: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
21: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
22: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
23: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
24: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
25: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
26: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
32: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
33: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
36: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
37: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
75: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
76: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
77: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
78: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
79: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
80: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
81: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
82: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
83: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
84: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
85: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
86: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
87: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
88: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
89: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
90: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
91: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
92: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
93: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
94: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
95: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
112: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
113: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
114: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
115: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
116: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
117: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
118: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
119: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
120: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
121: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
122: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
123: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
124: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
125: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
126: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
127: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
|
||||
152: archive.GameVariableInfoFor("Bridge Separated").At(0),
|
||||
|
||||
160: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
161: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
162: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
163: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
164: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
165: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
166: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
167: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
168: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
169: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
192: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
193: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
194: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
195: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
196: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
197: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
198: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
199: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
200: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
201: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
202: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
203: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
204: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
205: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
206: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
207: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
225: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
227: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
229: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
231: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
233: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
235: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
237: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
239: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
241: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
243: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
245: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
247: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
249: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
251: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
253: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
255: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
257: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
259: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
261: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
263: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
265: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
267: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
269: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
271: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
273: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
275: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
277: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
279: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
281: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
283: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
285: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
287: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
289: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
291: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
293: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
295: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
297: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
299: archive.GameVariableInfoFor("(unknown)").At(1),
|
||||
}
|
||||
|
||||
// IntegerVariable returns a variable info for integer variables.
|
||||
func IntegerVariable(index int) *archive.GameVariableInfo {
|
||||
varInfo := integerVariables.Lookup(index)
|
||||
if varInfo != nil {
|
||||
return varInfo
|
||||
}
|
||||
varInfo = archive.EngineIntegerVariable(index)
|
||||
if varInfo == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if index == 31 {
|
||||
varInfo.Name = "Reactor Code 1"
|
||||
}
|
||||
if index == 32 {
|
||||
varInfo.Name = "Reactor Code 2"
|
||||
}
|
||||
return varInfo
|
||||
}
|
||||
|
||||
// BooleanVariable returns a variable info for boolean variables.
|
||||
func BooleanVariable(index int) *archive.GameVariableInfo {
|
||||
varInfo := booleanVariables.Lookup(index)
|
||||
if varInfo != nil {
|
||||
return varInfo
|
||||
}
|
||||
return archive.EngineBooleanVariable(index)
|
||||
}
|
||||
|
||||
// MissionVariables is a collector of mission-specific variable accessors.
|
||||
type MissionVariables struct{}
|
||||
|
||||
var unknownVar = archive.GameVariableInfoFor("(unknown)").At(0)
|
||||
|
||||
// IntegerVariable returns a variable info for given index.
|
||||
func (vars MissionVariables) IntegerVariable(index int) archive.GameVariableInfo {
|
||||
varInfo := IntegerVariable(index)
|
||||
if varInfo == nil {
|
||||
return unknownVar
|
||||
}
|
||||
return *varInfo
|
||||
}
|
||||
|
||||
// BooleanVariable returns a variable info for given index.
|
||||
func (vars MissionVariables) BooleanVariable(index int) archive.GameVariableInfo {
|
||||
varInfo := BooleanVariable(index)
|
||||
if varInfo == nil {
|
||||
return unknownVar
|
||||
}
|
||||
return *varInfo
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/*
|
||||
Package world and its subpackages contain everything a typical simulated world requires.
|
||||
*/
|
||||
package world
|
||||
@@ -0,0 +1,61 @@
|
||||
package ids
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/resource"
|
||||
)
|
||||
|
||||
// Filename defines a wrapper for a file that is possibly language-specific.
|
||||
type Filename interface {
|
||||
// For returns the name of the file for the given language.
|
||||
For(lang resource.Language) string
|
||||
|
||||
// Matches returns true if the given filename matches the described one.
|
||||
Matches(filename string) bool
|
||||
}
|
||||
|
||||
// I18nFile is for internationalized resource files - i.e., those that store resources per file.
|
||||
type I18nFile [resource.LanguageCount]string
|
||||
|
||||
// For returns the string per language index.
|
||||
func (spec I18nFile) For(lang resource.Language) string {
|
||||
return spec[int(lang)]
|
||||
}
|
||||
|
||||
// Matches returns true if the given filename matches one of the localized filenames.
|
||||
func (spec I18nFile) Matches(filename string) bool {
|
||||
lowercase := strings.ToLower(filename)
|
||||
for _, entry := range spec {
|
||||
if entry == lowercase {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// AnyLanguage is for generic resource files that are language agnostic.
|
||||
type AnyLanguage string
|
||||
|
||||
// For returns the string itself.
|
||||
func (any AnyLanguage) For(lang resource.Language) string {
|
||||
return string(any)
|
||||
}
|
||||
|
||||
// Matches returns true if the given filename matches this one.
|
||||
func (any AnyLanguage) Matches(filename string) bool {
|
||||
return strings.ToLower(filename) == string(any)
|
||||
}
|
||||
|
||||
// FilenameList is a list of filenames.
|
||||
type FilenameList []Filename
|
||||
|
||||
// Matches returns true if the given filename matches any of the contained entries.
|
||||
func (list FilenameList) Matches(filename string) bool {
|
||||
for _, entry := range list {
|
||||
if entry.Matches(filename) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package ids
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/resource"
|
||||
)
|
||||
|
||||
// CybStrng contains all strings.
|
||||
var CybStrng = I18nFile([resource.LanguageCount]string{"cybstrng.res", "frnstrng.res", "gerstrng.res"})
|
||||
|
||||
// MfdArt contains all MFD graphics.
|
||||
var MfdArt = I18nFile([resource.LanguageCount]string{"mfdart.res", "mfdfrn.res", "mfdger.res"})
|
||||
|
||||
// CitALog contains all log audio.
|
||||
var CitALog = I18nFile([resource.LanguageCount]string{"citalog.res", "frnalog.res", "geralog.res"})
|
||||
|
||||
// CitBark contains all bark audio.
|
||||
var CitBark = I18nFile([resource.LanguageCount]string{"citbark.res", "frnbark.res", "gerbark.res"})
|
||||
|
||||
// LowIntr contains the low-res intro video.
|
||||
var LowIntr = I18nFile([resource.LanguageCount]string{"lowintr.res", "lofrintr.res", "logeintr.res"})
|
||||
|
||||
// SvgaIntr contains the high-res intro video.
|
||||
var SvgaIntr = I18nFile([resource.LanguageCount]string{"svgaintr.res", "svfrintr.res", "svgeintr.res"})
|
||||
|
||||
// Archive contains the game world.
|
||||
var Archive = AnyLanguage("archive.dat")
|
||||
|
||||
// GamePal contains the game palettes.
|
||||
var GamePal = AnyLanguage("gamepal.res")
|
||||
|
||||
// GameScr contains big bitmaps.
|
||||
var GameScr = AnyLanguage("gamescr.res")
|
||||
|
||||
// Texture contains all textures.
|
||||
var Texture = AnyLanguage("texture.res")
|
||||
|
||||
// VidMail contains all video mails.
|
||||
var VidMail = AnyLanguage("vidmail.res")
|
||||
|
||||
// Death contains another death video.
|
||||
var Death = AnyLanguage("death.res")
|
||||
|
||||
// Intro contains another intro video.
|
||||
var Intro = AnyLanguage("intro.res")
|
||||
|
||||
// Start1 contains ... ?.
|
||||
var Start1 = AnyLanguage("start1.res")
|
||||
|
||||
// Win1 contains ... ?.
|
||||
var Win1 = AnyLanguage("win1.res")
|
||||
|
||||
// LowDeth contains the low-res death video.
|
||||
var LowDeth = AnyLanguage("lowdeth.res")
|
||||
|
||||
// LowEnd contains the low-res end video.
|
||||
var LowEnd = AnyLanguage("lowend.res")
|
||||
|
||||
// SvgaDeth contains the high-res death video.
|
||||
var SvgaDeth = AnyLanguage("svgadeth.res")
|
||||
|
||||
// SvgaEnd contains the high-res end video.
|
||||
var SvgaEnd = AnyLanguage("svgaend.res")
|
||||
|
||||
// Obj3D contains 3D objects.
|
||||
var Obj3D = AnyLanguage("obj3D.res")
|
||||
|
||||
// ObjArt contains object art.
|
||||
var ObjArt = AnyLanguage("objart.res")
|
||||
|
||||
// ObjArt2 contains further object art.
|
||||
var ObjArt2 = AnyLanguage("objart2.res")
|
||||
|
||||
// ObjArt3 contains further object art.
|
||||
var ObjArt3 = AnyLanguage("objart3.res")
|
||||
|
||||
// CitMat contains materials for 3D objects.
|
||||
var CitMat = AnyLanguage("citmat.res")
|
||||
|
||||
// CutsPal contains palettes for the cutscenes.
|
||||
var CutsPal = AnyLanguage("cutspal.res")
|
||||
|
||||
// HandArt contains bitmaps for grabbed things.
|
||||
var HandArt = AnyLanguage("handart.res")
|
||||
|
||||
// SideArt contains bitmaps for the side buttons.
|
||||
var SideArt = AnyLanguage("sideart.res")
|
||||
|
||||
// DigiFX contains all the effect sounds.
|
||||
var DigiFX = AnyLanguage("digifx.res")
|
||||
|
||||
// Splash contains the splash screens.
|
||||
var Splash = AnyLanguage("splash.res")
|
||||
|
||||
// SplshPal contains the splash screen palettes.
|
||||
var SplshPal = AnyLanguage("splspal.res")
|
||||
|
||||
// LowResVideos returns the filename descriptors of all low-res videos.
|
||||
func LowResVideos() FilenameList {
|
||||
return []Filename{LowIntr, LowDeth, LowEnd, Intro, Start1, Win1}
|
||||
}
|
||||
|
||||
// HighResVideos returns the filename descriptors of all high-res videos.
|
||||
func HighResVideos() FilenameList {
|
||||
return []Filename{SvgaIntr, SvgaDeth, SvgaEnd}
|
||||
}
|
||||
|
||||
// LocalizedFiles returns the filename descriptors of all files that are localized.
|
||||
func LocalizedFiles() []Filename {
|
||||
return []Filename{CybStrng, MfdArt, CitALog, CitBark, LowIntr, SvgaIntr}
|
||||
}
|
||||
|
||||
// LocalizeFilename returns the language that the resource file would typically contain.
|
||||
func LocalizeFilename(filename string) resource.Language {
|
||||
all := LocalizedFiles()
|
||||
lowercase := strings.ToLower(filename)
|
||||
result := resource.LangAny
|
||||
for _, lang := range resource.Languages() {
|
||||
for _, file := range all {
|
||||
if file.For(lang) == lowercase {
|
||||
result = lang
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package ids_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/resource"
|
||||
"github.com/inkyblackness/hacked/ss1/world/ids"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestLocalizedResources(t *testing.T) {
|
||||
tt := []struct {
|
||||
filename string
|
||||
expected resource.Language
|
||||
}{
|
||||
{"citmat.res", resource.LangAny},
|
||||
{"digifx.res", resource.LangAny},
|
||||
{"gamescr.res", resource.LangAny},
|
||||
{"handart.res", resource.LangAny},
|
||||
{"intro.res", resource.LangAny},
|
||||
{"obj3d.res", resource.LangAny},
|
||||
{"objart2.res", resource.LangAny},
|
||||
{"objart3.res", resource.LangAny},
|
||||
{"sideart.res", resource.LangAny},
|
||||
{"texture.res", resource.LangAny},
|
||||
|
||||
{"cybstrng.res", resource.LangDefault},
|
||||
{"mfdart.res", resource.LangDefault},
|
||||
|
||||
{"frnstrng.res", resource.LangFrench},
|
||||
{"mfdfrn.res", resource.LangFrench},
|
||||
|
||||
{"gerstrng.res", resource.LangGerman},
|
||||
{"mfdger.res", resource.LangGerman},
|
||||
|
||||
{"archive.dat", resource.LangAny},
|
||||
{"cutspal.res", resource.LangAny},
|
||||
{"death.res", resource.LangAny},
|
||||
{"gamepal.res", resource.LangAny},
|
||||
{"intro.res", resource.LangAny},
|
||||
{"lowdeth.res", resource.LangAny},
|
||||
{"lowend.res", resource.LangAny},
|
||||
{"objart.res", resource.LangAny},
|
||||
{"splash.res", resource.LangAny},
|
||||
{"splshpal.res", resource.LangAny},
|
||||
{"start1.res", resource.LangAny},
|
||||
{"svgadeth.res", resource.LangAny},
|
||||
{"svgaend.res", resource.LangAny},
|
||||
{"vidmail.res", resource.LangAny},
|
||||
{"win1.res", resource.LangAny},
|
||||
|
||||
{"citalog.res", resource.LangDefault},
|
||||
{"citbark.res", resource.LangDefault},
|
||||
{"lowintr.res", resource.LangDefault},
|
||||
{"svgaintr.res", resource.LangDefault},
|
||||
|
||||
{"frnalog.res", resource.LangFrench},
|
||||
{"frnbark.res", resource.LangFrench},
|
||||
{"lofrintr.res", resource.LangFrench},
|
||||
{"svfrintr.res", resource.LangFrench},
|
||||
|
||||
{"geralog.res", resource.LangGerman},
|
||||
{"gerbark.res", resource.LangGerman},
|
||||
{"logeintr.res", resource.LangGerman},
|
||||
{"svgeintr.res", resource.LangGerman},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
result := ids.LocalizeFilename(tc.filename)
|
||||
assert.Equal(t, tc.expected, result, "Wrong language for <"+tc.filename+">")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package ids
|
||||
|
||||
import "github.com/inkyblackness/hacked/ss1/resource"
|
||||
|
||||
// Palette identifier are listed below.
|
||||
const (
|
||||
GamePalettesStart resource.ID = 0x02BC
|
||||
)
|
||||
|
||||
// Texture identifier are listed below.
|
||||
const (
|
||||
IconTextures resource.ID = 0x004C
|
||||
SmallTextures resource.ID = 0x004D
|
||||
MediumTextures resource.ID = 0x02C3
|
||||
LargeTextures resource.ID = 0x03E8
|
||||
|
||||
ScreenTextures resource.ID = 0x0141
|
||||
|
||||
TextureNames resource.ID = 0x086A
|
||||
TextureUsages resource.ID = 0x086B
|
||||
)
|
||||
|
||||
// Bitmap identifier are listed below.
|
||||
const (
|
||||
ObjectBitmaps resource.ID = 0x0546
|
||||
ObjectTextureBitmaps resource.ID = 0x01DB
|
||||
ObjectMaterialBitmaps resource.ID = 0x0884
|
||||
|
||||
IconBitmaps resource.ID = 0x004E
|
||||
GraffitiBitmaps resource.ID = 0x004F
|
||||
|
||||
MfdDataBitmaps resource.ID = 0x0028
|
||||
)
|
||||
|
||||
// Animation and video identifier are listed below.
|
||||
const (
|
||||
VideoMailBitmapsStart resource.ID = 0x0A40
|
||||
VideoMailAnimationsStart resource.ID = 0x0A4C
|
||||
)
|
||||
|
||||
// Movie identifier are listed below.
|
||||
const (
|
||||
MovieIntro resource.ID = 0x0BD6
|
||||
MovieDeath resource.ID = 0x0BD7
|
||||
MovieEnd resource.ID = 0x0BD8
|
||||
)
|
||||
|
||||
// Text identifier are listed below.
|
||||
const (
|
||||
PaperTextsStart resource.ID = 0x003C
|
||||
TrapMessageTexts resource.ID = 0x0867
|
||||
WordTexts resource.ID = 0x0868
|
||||
PanelNameTexts resource.ID = 0x0869
|
||||
LogCategoryTexts resource.ID = 0x0870
|
||||
VariousMessageTexts resource.ID = 0x0871
|
||||
ScreenMessageTexts resource.ID = 0x0877
|
||||
InfoNodeMessageTexts resource.ID = 0x0878
|
||||
AccessCardNameTexts resource.ID = 0x0879
|
||||
DataletMessageTexts resource.ID = 0x087A
|
||||
|
||||
ObjectLongNames resource.ID = 0x0024
|
||||
ObjectShortNames resource.ID = 0x086D
|
||||
)
|
||||
|
||||
// Message identifier are listed below.
|
||||
const (
|
||||
MailsStart resource.ID = 0x0989
|
||||
LogsStart resource.ID = 0x09B8
|
||||
FragmentsStart resource.ID = 0x0A98
|
||||
|
||||
MailsAudioStart resource.ID = 0x0989 + 300
|
||||
LogsAudioStart resource.ID = 0x09B8 + 300
|
||||
)
|
||||
|
||||
// Sound identifier are listed below.
|
||||
const (
|
||||
TrapMessagesAudioStart resource.ID = 0x0C1C
|
||||
|
||||
SoundEffectsAudioStart resource.ID = 0x00C9
|
||||
)
|
||||
|
||||
// Archive identifier are listed below.
|
||||
const (
|
||||
ArchiveName resource.ID = 0x0FA0
|
||||
GameState resource.ID = 0x0FA1
|
||||
|
||||
LevelResourcesStart resource.ID = 4000
|
||||
)
|
||||
@@ -0,0 +1,161 @@
|
||||
package ids
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/hacked/ss1/content/archive"
|
||||
"github.com/inkyblackness/hacked/ss1/content/archive/level/lvlids"
|
||||
"github.com/inkyblackness/hacked/ss1/content/object"
|
||||
"github.com/inkyblackness/hacked/ss1/resource"
|
||||
)
|
||||
|
||||
// ResourceInfo describes a group of resources with their default serialization properties.
|
||||
type ResourceInfo struct {
|
||||
// StartID is the first ID of the resource block (inclusive).
|
||||
StartID resource.ID
|
||||
// EndID is the last ID of the resource block (exclusive).
|
||||
EndID resource.ID
|
||||
|
||||
// ContentType describes how to interpret resource data.
|
||||
ContentType resource.ContentType
|
||||
// Compound indicates whether the resource has a variable amount of blocks. Simple resources always have one block.
|
||||
Compound bool
|
||||
// Compressed indicates that the resource data shall be stored in compressed form.
|
||||
Compressed bool
|
||||
|
||||
// List is set for compound resources that have an atomic resource per block.
|
||||
List bool
|
||||
// MaxCount describes how many resources can be stored at maximum. Zero for unlimited / defined somewhere else.
|
||||
MaxCount int
|
||||
|
||||
// ResFile specifies the .res file in which this resource should be stored.
|
||||
ResFile Filename
|
||||
}
|
||||
|
||||
// Info returns the resource information for the identified resource.
|
||||
func Info(id resource.ID) (ResourceInfo, bool) {
|
||||
info, existing := infoByID[id]
|
||||
return info, existing
|
||||
}
|
||||
|
||||
func init() {
|
||||
register := func(info ResourceInfo) {
|
||||
count := info.EndID.Value() - info.StartID.Value()
|
||||
for offset := 0; offset < int(count); offset++ {
|
||||
infoByID[info.StartID.Plus(offset)] = info
|
||||
}
|
||||
}
|
||||
for _, info := range infoList {
|
||||
register(info)
|
||||
}
|
||||
levelInfo := func(lvl int, lvlResID int, compressed bool) ResourceInfo {
|
||||
resID := LevelResourcesStart.Plus(lvl*lvlids.PerLevel + lvlResID)
|
||||
return ResourceInfo{
|
||||
StartID: resID,
|
||||
EndID: resID.Plus(1),
|
||||
|
||||
ContentType: resource.Archive,
|
||||
Compound: false,
|
||||
Compressed: compressed,
|
||||
|
||||
List: false,
|
||||
MaxCount: 1,
|
||||
|
||||
ResFile: Archive,
|
||||
}
|
||||
}
|
||||
// In the following, the compression flags were set if the vanilla archive had
|
||||
// the respective resource compressed in at least one level.
|
||||
// Most of them were compressed uniform across all levels, some had exceptions.
|
||||
for lvl := 0; lvl < archive.MaxLevels; lvl++ {
|
||||
register(levelInfo(lvl, lvlids.MapVersionNumber, false))
|
||||
register(levelInfo(lvl, lvlids.ObjectVersionNumber, false))
|
||||
register(levelInfo(lvl, lvlids.Information, true))
|
||||
|
||||
register(levelInfo(lvl, lvlids.TileMap, true))
|
||||
register(levelInfo(lvl, lvlids.Schedules, false))
|
||||
register(levelInfo(lvl, lvlids.TextureAtlas, false))
|
||||
register(levelInfo(lvl, lvlids.ObjectMainTable, true))
|
||||
register(levelInfo(lvl, lvlids.ObjectCrossRefTable, true))
|
||||
for class := 0; class < object.ClassCount; class++ {
|
||||
register(levelInfo(lvl, lvlids.ObjectClassTablesStart+class, true))
|
||||
register(levelInfo(lvl, lvlids.ObjectDefaultTablesStart+class, true))
|
||||
}
|
||||
|
||||
register(levelInfo(lvl, lvlids.SavefileVersion, false))
|
||||
register(levelInfo(lvl, lvlids.Unused41, false))
|
||||
|
||||
register(levelInfo(lvl, lvlids.TextureAnimations, true))
|
||||
register(levelInfo(lvl, lvlids.SurveillanceSources, true))
|
||||
register(levelInfo(lvl, lvlids.SurveillanceSurrogates, true))
|
||||
register(levelInfo(lvl, lvlids.Parameters, true))
|
||||
register(levelInfo(lvl, lvlids.MapNotes, true))
|
||||
register(levelInfo(lvl, lvlids.MapNotesPointer, false))
|
||||
|
||||
register(levelInfo(lvl, lvlids.Unknown48, true))
|
||||
register(levelInfo(lvl, lvlids.Unknown49, true))
|
||||
register(levelInfo(lvl, lvlids.Unknown50, false))
|
||||
|
||||
register(levelInfo(lvl, lvlids.LoopConfiguration, true))
|
||||
|
||||
register(levelInfo(lvl, lvlids.Unknown52, false))
|
||||
register(levelInfo(lvl, lvlids.HeightSemaphores, true))
|
||||
}
|
||||
}
|
||||
|
||||
var infoByID = map[resource.ID]ResourceInfo{}
|
||||
|
||||
var infoList = []ResourceInfo{
|
||||
{GamePalettesStart, GamePalettesStart.Plus(3), resource.Palette, false, false, false, 3, GamePal},
|
||||
|
||||
{IconTextures, IconTextures.Plus(1), resource.Bitmap, true, false, true, 293, Texture},
|
||||
{SmallTextures, SmallTextures.Plus(1), resource.Bitmap, true, false, true, 293, Texture},
|
||||
{MediumTextures, MediumTextures.Plus(293), resource.Bitmap, true, false, false, 293, Texture},
|
||||
{LargeTextures, LargeTextures.Plus(293), resource.Bitmap, true, false, false, 293, Texture},
|
||||
{TextureNames, TextureNames.Plus(1), resource.Text, true, false, true, 293, CybStrng},
|
||||
{TextureUsages, TextureUsages.Plus(1), resource.Text, true, false, true, 293, CybStrng},
|
||||
|
||||
{ObjectBitmaps, ObjectBitmaps.Plus(1), resource.Bitmap, true, false, true, 0, ObjArt},
|
||||
{ObjectTextureBitmaps, ObjectTextureBitmaps.Plus(64), resource.Bitmap, true, false, false, 64, CitMat},
|
||||
{ObjectMaterialBitmaps, ObjectMaterialBitmaps.Plus(32), resource.Bitmap, true, false, false, 32, CitMat},
|
||||
|
||||
{ScreenTextures, ScreenTextures.Plus(102), resource.Bitmap, true, false, false, 102, Texture},
|
||||
|
||||
{IconBitmaps, IconBitmaps.Plus(1), resource.Bitmap, true, false, true, 64, ObjArt3},
|
||||
{GraffitiBitmaps, GraffitiBitmaps.Plus(1), resource.Bitmap, true, false, true, 64, ObjArt3},
|
||||
|
||||
{MfdDataBitmaps, MfdDataBitmaps.Plus(1), resource.Bitmap, true, true, true, 256, MfdArt},
|
||||
|
||||
{VideoMailBitmapsStart, VideoMailBitmapsStart.Plus(12), resource.Bitmap, true, false, false, 12, VidMail},
|
||||
{VideoMailAnimationsStart, VideoMailAnimationsStart.Plus(12), resource.Animation, true, false, false, 12, VidMail},
|
||||
|
||||
{MovieIntro, MovieIntro.Plus(1), resource.Movie, false, false, false, 1, SvgaIntr},
|
||||
{MovieDeath, MovieDeath.Plus(1), resource.Movie, false, false, false, 1, SvgaDeth},
|
||||
{MovieEnd, MovieEnd.Plus(1), resource.Movie, false, false, false, 1, SvgaEnd},
|
||||
|
||||
{PaperTextsStart, PaperTextsStart.Plus(16), resource.Text, true, false, false, 16, CybStrng},
|
||||
|
||||
{TrapMessageTexts, TrapMessageTexts.Plus(1), resource.Text, true, false, true, 256, CybStrng},
|
||||
{TrapMessagesAudioStart, TrapMessagesAudioStart.Plus(256), resource.Movie, false, false, false, 256, CitBark},
|
||||
|
||||
{SoundEffectsAudioStart, SoundEffectsAudioStart.Plus(114), resource.Sound, false, false, false, 114, DigiFX},
|
||||
|
||||
{WordTexts, WordTexts.Plus(1), resource.Text, true, false, true, 512, CybStrng},
|
||||
{PanelNameTexts, PanelNameTexts.Plus(1), resource.Text, true, false, true, 256, CybStrng},
|
||||
{LogCategoryTexts, LogCategoryTexts.Plus(1), resource.Text, true, false, true, 16, CybStrng},
|
||||
{VariousMessageTexts, VariousMessageTexts.Plus(1), resource.Text, true, false, true, 256, CybStrng},
|
||||
{ScreenMessageTexts, ScreenMessageTexts.Plus(1), resource.Text, true, false, true, 120, CybStrng},
|
||||
{InfoNodeMessageTexts, InfoNodeMessageTexts.Plus(1), resource.Text, true, false, true, 256, CybStrng},
|
||||
{AccessCardNameTexts, AccessCardNameTexts.Plus(1), resource.Text, true, false, true, 32 * 2, CybStrng},
|
||||
{DataletMessageTexts, DataletMessageTexts.Plus(1), resource.Text, true, false, true, 256, CybStrng},
|
||||
|
||||
{MailsStart, MailsStart.Plus(47), resource.Text, true, false, false, 47, CybStrng},
|
||||
{LogsStart, LogsStart.Plus(136), resource.Text, true, false, false, 136, CybStrng},
|
||||
{FragmentsStart, FragmentsStart.Plus(16), resource.Text, true, false, false, 16, CybStrng},
|
||||
{MailsAudioStart, MailsAudioStart.Plus(47), resource.Movie, false, false, false, 47, CitALog},
|
||||
{LogsAudioStart, LogsAudioStart.Plus(224), resource.Movie, false, false, false, 224, CitALog},
|
||||
|
||||
{ObjectLongNames, ObjectLongNames.Plus(1), resource.Text, true, false, true, 0, CybStrng},
|
||||
{ObjectShortNames, ObjectShortNames.Plus(1), resource.Text, true, false, true, 0, CybStrng},
|
||||
|
||||
{ArchiveName, ArchiveName.Plus(1), resource.Archive, false, false, false, 1, Archive},
|
||||
{GameState, GameState.Plus(1), resource.Archive, false, true, false, 1, Archive},
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
package ids
|
||||
|
||||
// SoundEffectInfo describes one sound effect in the game.
|
||||
type SoundEffectInfo struct {
|
||||
// Name is the unique identifier for the effect source.
|
||||
Name string
|
||||
// Index refers to the audio index. Multiple effects may use the same audio. -1 indicates no audio mapped.
|
||||
AudioIndex int
|
||||
}
|
||||
|
||||
// SoundEffectsForAudio returns the effect information that share the same audio.
|
||||
func SoundEffectsForAudio(index int) []SoundEffectInfo {
|
||||
var result []SoundEffectInfo
|
||||
for _, effect := range soundEffects {
|
||||
if effect.AudioIndex == index {
|
||||
result = append(result, effect)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// The following table is based on the constants found in sfxlist.h (non-demo case).
|
||||
var soundEffects = []SoundEffectInfo{
|
||||
// Doors
|
||||
{Name: "DoorMetal", AudioIndex: 3},
|
||||
{Name: "DoorNormal", AudioIndex: 5},
|
||||
{Name: "DoorIris", AudioIndex: 6},
|
||||
{Name: "DoorBulkhead", AudioIndex: 67},
|
||||
{Name: "DoorGrating", AudioIndex: 90},
|
||||
|
||||
// Ambient
|
||||
{Name: "Bridge1", AudioIndex: -1},
|
||||
{Name: "Bridge2", AudioIndex: -1},
|
||||
{Name: "Bridge3", AudioIndex: -1},
|
||||
{Name: "Bridge4", AudioIndex: -1},
|
||||
{Name: "Maint1", AudioIndex: 9},
|
||||
{Name: "Maint2", AudioIndex: -1},
|
||||
{Name: "Maint3", AudioIndex: -1},
|
||||
{Name: "Maint4", AudioIndex: -1},
|
||||
{Name: "Grove1", AudioIndex: 43},
|
||||
|
||||
// Critters
|
||||
{Name: "Death1", AudioIndex: 11},
|
||||
{Name: "Death2", AudioIndex: 49},
|
||||
{Name: "Death3", AudioIndex: 50},
|
||||
{Name: "Death4", AudioIndex: 51},
|
||||
{Name: "Death5", AudioIndex: 53},
|
||||
{Name: "Death6", AudioIndex: 54},
|
||||
{Name: "Death7", AudioIndex: 68},
|
||||
{Name: "Death8", AudioIndex: 69},
|
||||
{Name: "Death9", AudioIndex: 88},
|
||||
{Name: "Death10", AudioIndex: 93},
|
||||
{Name: "Death11", AudioIndex: 101},
|
||||
{Name: "Attack1", AudioIndex: 12},
|
||||
{Name: "Attack4", AudioIndex: 46},
|
||||
{Name: "Attack5", AudioIndex: 48},
|
||||
{Name: "Attack6", AudioIndex: 52},
|
||||
{Name: "Attack7", AudioIndex: 55},
|
||||
{Name: "Attack8", AudioIndex: 63},
|
||||
{Name: "Attack9", AudioIndex: 16},
|
||||
{Name: "Notice1", AudioIndex: 58},
|
||||
{Name: "Notice2", AudioIndex: 59},
|
||||
{Name: "Notice3", AudioIndex: 74},
|
||||
{Name: "Notice4", AudioIndex: 75},
|
||||
{Name: "Notice5", AudioIndex: 100},
|
||||
{Name: "Near1", AudioIndex: 73},
|
||||
{Name: "Near2", AudioIndex: 56},
|
||||
{Name: "Near3", AudioIndex: 47},
|
||||
{Name: "Near4", AudioIndex: 25},
|
||||
|
||||
// Wacky Objects
|
||||
{Name: "Repulsor", AudioIndex: -1},
|
||||
{Name: "ForceBridge", AudioIndex: 72},
|
||||
{Name: "TerrainElevLoop", AudioIndex: 15},
|
||||
{Name: "SparkingCable", AudioIndex: 87},
|
||||
{Name: "SurgeryMachine", AudioIndex: 102},
|
||||
|
||||
// Combat
|
||||
{Name: "GunMinipistol", AudioIndex: 39},
|
||||
{Name: "GunDartpistol", AudioIndex: 86},
|
||||
{Name: "GunMagnum", AudioIndex: 40},
|
||||
{Name: "GunAssault", AudioIndex: 17},
|
||||
{Name: "GunRiot", AudioIndex: 41},
|
||||
{Name: "GunFlechette", AudioIndex: 38},
|
||||
{Name: "GunSkorpion", AudioIndex: 65},
|
||||
{Name: "GunMagpulse", AudioIndex: 45},
|
||||
{Name: "GunRailgun", AudioIndex: 29},
|
||||
{Name: "GunPipeHitMeat", AudioIndex: 4},
|
||||
{Name: "GunPipeHitMetal", AudioIndex: 21},
|
||||
{Name: "GunPipeMiss", AudioIndex: 24},
|
||||
{Name: "GunLaserepeeHit", AudioIndex: 31},
|
||||
{Name: "GunLaserepeeMiss", AudioIndex: 34},
|
||||
{Name: "GunPhaser", AudioIndex: 18},
|
||||
{Name: "GunBlaster", AudioIndex: 94},
|
||||
{Name: "GunIonbeam", AudioIndex: 95},
|
||||
{Name: "GunStungun", AudioIndex: 19},
|
||||
{Name: "GunPlasma", AudioIndex: 97},
|
||||
|
||||
{Name: "PlayerHurt", AudioIndex: 64},
|
||||
{Name: "Shield1", AudioIndex: 32},
|
||||
{Name: "Shield2", AudioIndex: 20},
|
||||
{Name: "ShieldUp", AudioIndex: 96},
|
||||
{Name: "ShieldDown", AudioIndex: 42},
|
||||
{Name: "MetalSpang", AudioIndex: 89},
|
||||
{Name: "Radiation", AudioIndex: 2},
|
||||
|
||||
{Name: "Reload1", AudioIndex: 22},
|
||||
{Name: "Reload2", AudioIndex: 23},
|
||||
{Name: "GrenadeArm", AudioIndex: 7},
|
||||
{Name: "BatteryUse", AudioIndex: 28},
|
||||
|
||||
{Name: "Explosion1", AudioIndex: 44},
|
||||
{Name: "Rumble", AudioIndex: 106},
|
||||
{Name: "Teleport", AudioIndex: 103},
|
||||
|
||||
{Name: "MonitorExplode", AudioIndex: 57},
|
||||
{Name: "CameraExplode", AudioIndex: 8},
|
||||
{Name: "CpuExplode", AudioIndex: 10},
|
||||
{Name: "DestroyCrate", AudioIndex: 37},
|
||||
{Name: "DestroyBarrel", AudioIndex: 109},
|
||||
|
||||
// Cspace
|
||||
{Name: "Pulser", AudioIndex: -1},
|
||||
{Name: "Drill", AudioIndex: -1},
|
||||
{Name: "Disc", AudioIndex: -1},
|
||||
{Name: "Datastorm", AudioIndex: -1},
|
||||
|
||||
{Name: "Recall", AudioIndex: -1},
|
||||
{Name: "Turbo", AudioIndex: -1},
|
||||
{Name: "Fakeid", AudioIndex: -1},
|
||||
{Name: "Decoy", AudioIndex: -1},
|
||||
|
||||
{Name: "EnterCspace", AudioIndex: 27},
|
||||
{Name: "OttoShodan", AudioIndex: 30},
|
||||
{Name: "CyberDamage", AudioIndex: -1},
|
||||
{Name: "Cyberheal", AudioIndex: -1},
|
||||
{Name: "Cybertoggle", AudioIndex: -1},
|
||||
{Name: "IceDefense", AudioIndex: -1},
|
||||
|
||||
{Name: "CyberAttack1", AudioIndex: -1},
|
||||
{Name: "CyberAttack2", AudioIndex: -1},
|
||||
{Name: "CyberAttack3", AudioIndex: -1},
|
||||
|
||||
// MFD and UI Wackiness
|
||||
{Name: "VideoDown", AudioIndex: 33},
|
||||
{Name: "MfdButton", AudioIndex: 35},
|
||||
{Name: "InventButton", AudioIndex: 79},
|
||||
{Name: "InventSelect", AudioIndex: 80},
|
||||
{Name: "InventAdd", AudioIndex: 81},
|
||||
{Name: "InventWare", AudioIndex: 82},
|
||||
{Name: "PatchUse", AudioIndex: 83},
|
||||
{Name: "ZoomBox", AudioIndex: 84},
|
||||
{Name: "MapZoom", AudioIndex: 85},
|
||||
{Name: "MfdKeypad", AudioIndex: 76},
|
||||
{Name: "MfdBuzz", AudioIndex: 77},
|
||||
{Name: "MfdSuccess", AudioIndex: 78},
|
||||
{Name: "Goggle", AudioIndex: 0},
|
||||
{Name: "Hudfrob", AudioIndex: 1},
|
||||
{Name: "Static", AudioIndex: 2},
|
||||
{Name: "Email", AudioIndex: 107},
|
||||
|
||||
// SHODAN
|
||||
{Name: "ShodanBark", AudioIndex: 30},
|
||||
{Name: "ShodanWeak", AudioIndex: 30},
|
||||
{Name: "ShodanStrong", AudioIndex: 30},
|
||||
|
||||
// Other
|
||||
{Name: "PanelSuccess", AudioIndex: 78},
|
||||
{Name: "PowerOut", AudioIndex: 26},
|
||||
{Name: "EnergyDrain", AudioIndex: 14},
|
||||
{Name: "EnergyRecharge", AudioIndex: 98},
|
||||
{Name: "Surge", AudioIndex: 60},
|
||||
{Name: "Vmail", AudioIndex: 92},
|
||||
{Name: "DropItem", AudioIndex: 99},
|
||||
|
||||
// 3d World
|
||||
{Name: "Button", AudioIndex: 71},
|
||||
{Name: "MechButton", AudioIndex: 36},
|
||||
{Name: "Bigbutton", AudioIndex: 61},
|
||||
{Name: "NormalLever", AudioIndex: 62},
|
||||
{Name: "Biglever", AudioIndex: 62},
|
||||
{Name: "Klaxon", AudioIndex: 70},
|
||||
|
||||
// Plot
|
||||
{Name: "GroveJett", AudioIndex: 66},
|
||||
|
||||
// Extra
|
||||
{Name: "PanelConfirm", AudioIndex: 13},
|
||||
{Name: "Death12", AudioIndex: 104},
|
||||
{Name: "Death13", AudioIndex: 105},
|
||||
{Name: "Death14", AudioIndex: 108},
|
||||
{Name: "Attack10", AudioIndex: 91},
|
||||
{Name: "Attack11", AudioIndex: 110},
|
||||
{Name: "Near5", AudioIndex: 111},
|
||||
{Name: "Death15", AudioIndex: 112},
|
||||
{Name: "FunPackAttackingUs", AudioIndex: 113},
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
// Package ids contains constants for all necessary identifier used in the world.
|
||||
// This package contains resource identifier, filenames and other constants necessary for storage.
|
||||
package ids
|
||||
Reference in New Issue
Block a user