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