Files
questshock/modding/hacked/ss1/world/Mod.go
T

325 lines
11 KiB
Go

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)
}
}
}
}
})
}