diff --git a/Plugins/BiowareLocalizationPlugin/BW LocaliziationResourceBits.txt b/Plugins/BiowareLocalizationPlugin/BW LocaliziationResourceBits.txt
new file mode 100644
index 000000000..61762e68b
--- /dev/null
+++ b/Plugins/BiowareLocalizationPlugin/BW LocaliziationResourceBits.txt
@@ -0,0 +1,68 @@
+Part: Type and what Byte position after this
+
+MetaData (16 bytes, located in Frosty at Resource.resMeta, not part of the byte count )
+ {
+ uint dataOffset
+ 3x byte value 0x0
+ }
+Header
+ {
+ uint magic = 0xd78b40eb (pos 4)
+ uint ??? (pos 8) // doesn't seem to affect anything
+ uint dataOffset (pos 12) // not actually used, instead the one from the metadata is the base for the game
+ 3x uint ??? (pos 24) // doens't seem to affect anything
+
+ uint nodeCount (pos 28)
+ // nodeCount is an even integer! The rootNode as would-be last node in the node list is *not* actually part of the list!
+
+ uint nodeOffset (pos 32)
+
+ uint stringsCount (pos 36)
+ uint stringsOffset (pos 40)
+
+ // Until the nodeOffset is reached
+ // If there are three or more entries in here,
+ // then the corresponding 8ByteBlockData after the second one contain ids and bit offsets for the declinated adjectives for text parts used for crafted items in DA:I (Note that these ids are not the same accross languages!)
+ N times Unknown8ByteBlockCountAndOffset
+ {
+ uint unknownCounts
+ uint unknownOffset
+ }
+ }
+
+ // Position = nodeOffset -> pos most likely either 56 or 64
+HuffmanCoding
+ {
+ nodeCount x uint value == bitFlip char
+ }
+ (size = 4 per node)
+
+ // Position = stringsOffset
+StringData
+ {
+ stringsCount x
+ {
+ uint stringId
+ int stringIndex / positionOffset
+ }
+ (size = 8 per string)
+ }
+
+ // Position = Unknown8ByteBlockCountAndOffset[0].unknownOffset
+ // The next data blocks appears the same N times as their Unknown8ByteBlockCountAndOffset counterpart in the header
+ // Everything past the second of these blocks contains the bit offsets of the text pieces used for crafted item names in DA:I
+N times 8ByteBlockData
+ {
+ // Position = Unknown8ByteBlockCountAndOffset[index].unknownOffset
+ byte[].Length = Unknown8ByteBlockCountAndOffset[index].unknownCounts * 8
+ }
+
+ // Position = dataOffset
+Strings
+ {
+ stringsCount * HuffmanEncodedChars
+ }
+ // stringIndex / positionOffset = bit offset from dataOffset == textBefore bitOffset + textBefore bitlentgh
+ // last symbol (only symbol of empty string) is huffman node with letter 0x00! I.e., _value_ = 0xFF
+
+Remaining positions to full byte filled with 0s
\ No newline at end of file
diff --git a/Plugins/BiowareLocalizationPlugin/BioWareLocalizedStringEditorMenuExtension.cs b/Plugins/BiowareLocalizationPlugin/BioWareLocalizedStringEditorMenuExtension.cs
new file mode 100644
index 000000000..6be0a7b4b
--- /dev/null
+++ b/Plugins/BiowareLocalizationPlugin/BioWareLocalizedStringEditorMenuExtension.cs
@@ -0,0 +1,28 @@
+using BiowareLocalizationPlugin.Controls;
+using Frosty.Core;
+using FrostySdk;
+
+namespace BiowareLocalizationPlugin
+{
+ public class BioWareLocalizedStringEditorMenuExtension : MenuExtension
+ {
+
+ private const string ITEM_NAME = "Bioware Localized String Editor";
+
+ public override string TopLevelMenuName => "View";
+
+ public override string SubLevelMenuName => null;
+ public override string MenuItemName => ITEM_NAME;
+
+ public override RelayCommand MenuItemClicked => new RelayCommand((o) =>
+ {
+ if (ProfilesLibrary.DataVersion == (int)ProfileVersion.Anthem)
+ {
+ App.Logger.Log("Not applicable for Anthem, sorry for the inconvenience!");
+ return;
+ }
+ var textDb = (BiowareLocalizedStringDatabase) LocalizedStringDatabase.Current;
+ App.EditorWindow.OpenEditor(ITEM_NAME, new BiowareLocalizedStringEditor(textDb));
+ });
+ }
+}
diff --git a/Plugins/BiowareLocalizationPlugin/BiowareLocalizationCustomActionHandler.cs b/Plugins/BiowareLocalizationPlugin/BiowareLocalizationCustomActionHandler.cs
new file mode 100644
index 000000000..51765c31f
--- /dev/null
+++ b/Plugins/BiowareLocalizationPlugin/BiowareLocalizationCustomActionHandler.cs
@@ -0,0 +1,188 @@
+using BiowareLocalizationPlugin.LocalizedResources;
+using Frosty.Core;
+using Frosty.Core.IO;
+using Frosty.Core.Mod;
+using Frosty.Hash;
+using FrostySdk;
+using FrostySdk.IO;
+using FrostySdk.Managers;
+using FrostySdk.Resources;
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace BiowareLocalizationPlugin
+{
+ public class BiowareLocalizationCustomActionHandler : ICustomActionHandler
+ {
+
+ public HandlerUsage Usage => HandlerUsage.Merge;
+
+ // A mod is comprised of a series of base resources, embedded, ebx, res, and chunks. Embedded are used internally
+ // for the icon and images of a mod. Ebx/Res/Chunks are the core resources used for applying data to the game.
+ // When you create a custom handler, you need to provide your own resources for your custom handled data. This
+ // resource is unique however it is based on one of the three core types.
+ private class BiowareLocalizationModResource : EditorModResource
+ {
+
+ // Defines which type of resource this resource is.
+ public override ModResourceType Type => ModResourceType.Res;
+
+
+ // The resType is vital to be kept (its always LocalizedStringResource, but whatever)
+ private readonly uint m_resType;
+
+ // these other two fields may have to be written to the mod as well
+ private readonly ulong m_resRid;
+ private readonly byte[] m_resMeta;
+
+
+ public BiowareLocalizationModResource(ResAssetEntry inEntry, FrostyModWriter.Manifest inManifest) : base(inEntry)
+ {
+
+ // This constructor does the exact same thing as the ones in the TestPlugin
+
+ // obtain the modified data
+ ModifiedLocalizationResource md = inEntry.ModifiedEntry.DataObject as ModifiedLocalizationResource;
+ byte[] data = md.Save();
+
+ // store data and details about resource
+ name = inEntry.Name.ToLower();
+ sha1 = Utils.GenerateSha1(data);
+ resourceIndex = inManifest.Add(sha1, data);
+ size = data.Length;
+
+ // set the handler hash to the hash of the res type name
+ handlerHash = Fnv1.HashString(inEntry.Type.ToLower());
+
+ m_resType = inEntry.ResType;
+ m_resRid = inEntry.ResRid;
+ m_resMeta = inEntry.ResMeta;
+ }
+
+ ///
+ /// This method is calles when writing the mod. For Res Types it is vital that some additional information is persisted that is not written by the base method.
+ /// Mainly that is the ResourceType as uint
+ /// Additional data that is read, but I'm not sure whether it is actually necessary:
+ ///
+ ///
ResRid as ulong (not sure if this is really necessary, i.e., actually read)
+ ///
resMeta length
+ ///
resMeta as byte array
+ ///
+ ///
+ ///
+ public override void Write(NativeWriter writer)
+ {
+ base.Write(writer);
+
+ // write the required res type:
+ writer.Write(m_resType);
+
+ writer.Write(m_resRid);
+ writer.Write((m_resMeta != null) ? m_resMeta.Length : 0);
+ if (m_resMeta != null)
+ {
+ writer.Write(m_resMeta);
+ }
+ }
+ }
+
+ #region -- Editor Specific --
+
+ // This function is for writing resources to the mod file, this is where you would add your custom
+ // resources to be written.
+ public void SaveToMod(FrostyModWriter writer, AssetEntry entry)
+ {
+ writer.AddResource(new BiowareLocalizationModResource(entry as ResAssetEntry, writer.ResourceManifest));
+ }
+ #endregion
+
+ #region -- Mod Specific --
+
+ // This function is for the mod managers action view, to allow a handler to describe detailed actions performed
+ // format of the action string is ;; where action can be Modify or Merge (or Add!)
+ // and ResourceType can be Ebx,Res,Chunk
+ public IEnumerable GetResourceActions(string name, byte[] data)
+ {
+
+ if( !Config.Get(BiowareLocalizationPluginOptions.SHOW_INDIVIDUAL_TEXTIDS_OPTION_NAME, false, ConfigScope.Global))
+ {
+ return new List();
+ }
+
+ ModifiedLocalizationResource modified = ModifiedResource.Read(data) as ModifiedLocalizationResource;
+
+ List textIds = new List(modified.AlteredTexts.Keys);
+ textIds.Sort();
+
+ List resourceActions = new List(textIds.Count);
+ foreach (uint textId in textIds)
+ {
+ string resourceName = new StringBuilder(name).Append(" (0x").Append(textId.ToString("X8")).Append(')').ToString();
+ string resourceType = "res";
+ string action = "Modify";
+
+ resourceActions.Add(resourceName + ";" + resourceType + ";" + action);
+ }
+
+ return resourceActions;
+ }
+
+ // This function is invoked when a mod with such a handler is loaded, if a previous mod with a handler for this
+ // particular asset was loaded previously, then existing will be populated with that data, allowing this function
+ // the chance to merge the two datasets together
+ public object Load(object existing, byte[] newData)
+ {
+
+ ModifiedLocalizationResource edited = (ModifiedLocalizationResource)existing;
+ ModifiedLocalizationResource newTexts = (ModifiedLocalizationResource) ModifiedResource.Read(newData);
+
+ if(edited == null)
+ {
+ return newTexts;
+ }
+
+ edited.Merge(newTexts);
+
+ return edited;
+ }
+
+ // This function is invoked at the end of the mod loading, to actually modify the existing game data with the end
+ // result of the mod loaded data, it also allows for a handler to add new Resources to be replaced.
+ // ie. an Ebx handler might want to add a new Chunk resource that it is dependent on.
+ public void Modify(AssetEntry origEntry, AssetManager am, RuntimeResources runtimeResources, object data, out byte[] outData)
+ {
+
+ // no idea what frosty does if the resource does not exist in the local game, so first check for null:
+ if(origEntry == null)
+ {
+ outData = System.Array.Empty();
+ return;
+ }
+
+ // load the original resource
+ ResAssetEntry originalResAsset = am.GetResEntry(origEntry.Name);
+ ModifiedLocalizationResource modified = data as ModifiedLocalizationResource;
+ LocalizedStringResource resource = am.GetResAs(originalResAsset, modified);
+
+ // read about some weird null reference exception in here, so _maybe_ it was the resource?
+ if(resource == null)
+ {
+ throw new ArgumentNullException("resource", string.Format("Resource in BwLocalizationHandler Modify(...) is null after GetResAs call for <{0}>!", origEntry.Name));
+ }
+
+ byte[] uncompressedData = resource.SaveBytes();
+ outData = Utils.CompressFile(uncompressedData);
+
+ // update the metadata
+ byte[] alteredMetaData = resource.ResourceMeta;
+ ((ResAssetEntry)origEntry).ResMeta = alteredMetaData;
+
+ // update relevant asset entry values
+ origEntry.OriginalSize = uncompressedData.Length;
+ origEntry.Size = outData.Length;
+ origEntry.Sha1 = Utils.GenerateSha1(outData);
+ }
+ #endregion
+ }
+}
diff --git a/Plugins/BiowareLocalizationPlugin/BiowareLocalizationPlugin.csproj b/Plugins/BiowareLocalizationPlugin/BiowareLocalizationPlugin.csproj
index f84291f44..84f1caf1c 100644
--- a/Plugins/BiowareLocalizationPlugin/BiowareLocalizationPlugin.csproj
+++ b/Plugins/BiowareLocalizationPlugin/BiowareLocalizationPlugin.csproj
@@ -53,5 +53,9 @@
false
+
+ false
+
+
\ No newline at end of file
diff --git a/Plugins/BiowareLocalizationPlugin/BiowareLocalizationPluginModManagerOptions.cs b/Plugins/BiowareLocalizationPlugin/BiowareLocalizationPluginModManagerOptions.cs
new file mode 100644
index 000000000..5dcfb7183
--- /dev/null
+++ b/Plugins/BiowareLocalizationPlugin/BiowareLocalizationPluginModManagerOptions.cs
@@ -0,0 +1,46 @@
+
+using Frosty.Core;
+using FrostySdk.Attributes;
+
+namespace BiowareLocalizationPlugin
+{
+ [DisplayName("Bioware Localization Options")]
+ public class BiowareLocalizationPluginOptions : OptionsExtension
+ {
+
+ // The name for the global mod manager variable.
+ public static readonly string SHOW_INDIVIDUAL_TEXTIDS_OPTION_NAME = "BwLoMoShowIndividualTextIds";
+
+ public static readonly string ASK_XML_EXPORT_OPTIONS = "BwLoEoAskXmlExportOptions";
+
+ [Category("Mod Manager Options")]
+ [Description("If enabled, all individual text ids in each resource (res) are shown in the mod manager's Actions tab. Otherwise only the resource iteself is shown as merged. This setting is only for the mod manager and has no effect in the editor.")]
+ [DisplayName("Show Individual Text Ids")]
+ [EbxFieldMeta(FrostySdk.IO.EbxFieldType.Boolean)]
+ public bool ShowIndividualTextIds { get; set; } = false;
+
+ [Category("Editor Options")]
+ [DisplayName("Ask for Xml Export Options")]
+ [Description("If enabled, a popup prompt allows selecting whether to export all texts or only modified ones. If this value is false, then the default from below is used. This setting is only for the editor and has no effect in the mod manager.")]
+ [EbxFieldMeta(FrostySdk.IO.EbxFieldType.Boolean)]
+ public bool AskForXmlExportOptions { get; set; } = false;
+
+ public override void Load()
+ {
+ // mod manager
+ ShowIndividualTextIds = Config.Get(SHOW_INDIVIDUAL_TEXTIDS_OPTION_NAME, false, ConfigScope.Global);
+
+ // editor
+ AskForXmlExportOptions = Config.Get(ASK_XML_EXPORT_OPTIONS, false, ConfigScope.Global);
+ }
+
+ public override void Save()
+ {
+ // mod manager
+ Config.Add(SHOW_INDIVIDUAL_TEXTIDS_OPTION_NAME, ShowIndividualTextIds, ConfigScope.Global);
+
+ // editor
+ Config.Add(ASK_XML_EXPORT_OPTIONS, AskForXmlExportOptions, ConfigScope.Global);
+ }
+ }
+}
diff --git a/Plugins/BiowareLocalizationPlugin/BiowareLocalizedStringDatabase.cs b/Plugins/BiowareLocalizationPlugin/BiowareLocalizedStringDatabase.cs
index ad2145047..a4b564a2f 100644
--- a/Plugins/BiowareLocalizationPlugin/BiowareLocalizedStringDatabase.cs
+++ b/Plugins/BiowareLocalizationPlugin/BiowareLocalizedStringDatabase.cs
@@ -1,124 +1,434 @@
-using Frosty.Core;
+using BiowareLocalizationPlugin.Controls;
+using BiowareLocalizationPlugin.LocalizedResources;
+using Frosty.Core;
using FrostySdk.Managers;
using System;
using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Windows;
namespace BiowareLocalizationPlugin
{
public class BiowareLocalizedStringDatabase : ILocalizedStringDatabase
{
- private Dictionary strings = new Dictionary();
+ ///
+ /// The default language to operate with if no other one is given.
+ ///
+ public string DefaultLanguage { get; private set; }
+
+ ///
+ /// Holds all the languages supported by the local game and their bundles
+ ///
+ private SortedDictionary> m_languageLocalizationBundles;
+
+ ///
+ /// Dictionary of all currently loaded localized texts.
+ ///
+ private readonly Dictionary m_loadedLocalizedTextDBs = new Dictionary();
+
+ ///
+ /// marker whether or not this was already initialized.
+ ///
+ private bool m_initialized = false;
+
+ ///
+ /// Initializes the db.
+ ///
public void Initialize()
{
- LoadLocalizedStringConfiguration("LocalizedStringTranslationsConfiguration");
- LoadLocalizedStringConfiguration("LocalizedStringPatchTranslationsConfiguration");
+
+ DefaultLanguage = "LanguageFormat_" + Config.Get("Language", "English", scope: ConfigScope.Game);
+
+ if (m_initialized)
+ {
+ return;
+ }
+
+ m_languageLocalizationBundles = GetLanguageDictionary();
+
+ LanguageTextsDB defaultLocalizedTexts = new LanguageTextsDB();
+ defaultLocalizedTexts.Init(DefaultLanguage, m_languageLocalizationBundles[DefaultLanguage]);
+
+ m_loadedLocalizedTextDBs.Add(DefaultLanguage, defaultLocalizedTexts);
+
+ m_initialized = true;
+ }
+
+ ///
+ /// Tries to return the text for the given uid, throws an exception if the text id is not known.
+ /// @see #FindText
+ ///
+ ///
+ ///
+ public string GetString(uint id)
+ {
+ return GetText(DefaultLanguage, id);
+ }
+
+ public string GetString(string stringId)
+ {
+
+ bool canRead = uint.TryParse(stringId, NumberStyles.HexNumber, null, out uint textId);
+ if (canRead)
+ {
+ return GetString(textId);
+ }
+
+ App.Logger.LogError("Cannot read given textId <{0}>", stringId);
+ return stringId;
+
+ }
+
+ ///
+ /// Returns the language db for the requested language format, loading it if necessary.
+ ///
+ ///
+ ///
+ public LanguageTextsDB GetLocalizedTextDB(string languageFormat)
+ {
+ bool isLoaded = m_loadedLocalizedTextDBs.TryGetValue(languageFormat, out LanguageTextsDB localizedTextDb);
+ if (!isLoaded)
+ {
+ if (!m_languageLocalizationBundles.ContainsKey(languageFormat))
+ {
+ throw new ArgumentException(string.Format("LanguageFormat <{0}> does not exist in this game!", languageFormat));
+ }
+
+ localizedTextDb = new LanguageTextsDB();
+ localizedTextDb.Init(languageFormat, m_languageLocalizationBundles[languageFormat]);
+
+ m_loadedLocalizedTextDBs.Add(languageFormat, localizedTextDb);
+ }
+ return localizedTextDb;
+ }
+
+ ///
+ /// Tries to return the text for the given uid. Returns an error message if the text does not exist.
+ ///
+ ///
+ ///
+ ///
+ public string GetText(string languageFormat, uint textId)
+ {
+ return GetLocalizedTextDB(languageFormat).GetText(textId);
}
public IEnumerable EnumerateStrings()
{
- foreach (uint key in strings.Keys)
- yield return key;
+ return GetAllTextIds(DefaultLanguage);
}
- public IEnumerable EnumerateModifiedStrings()
+ ///
+ /// Returns a language specific list of all text ids.
+ ///
+ ///
+ ///
+ public IEnumerable GetAllTextIds(string languageFormat)
{
- throw new NotImplementedException();
+ return GetLocalizedTextDB(languageFormat).GetAllTextIds();
}
- public string GetString(uint id)
+ ///
+ /// Returns only the ids of modified or new texts.
+ ///
+ ///
+ ///
+ public IEnumerable GetAllModifiedTextsIds(string languageFormat)
+ {
+ return GetLocalizedTextDB(languageFormat).GetAllModifiedTextsIds();
+ }
+
+ ///
+ /// Tries to return the text for the given uid, returns null if the textid does not exist.
+ /// @see #GetString
+ ///
+ ///
+ ///
+ ///
+ public string FindText(string languageFormat, uint textId)
+ {
+ return GetLocalizedTextDB(languageFormat).FindText(textId);
+ }
+
+ ///
+ /// Returns the list of LocalizedStringResource in which the given text id can be found.
+ ///
+ ///
+ /// The text id to look for.
+ /// All resources in which the text id can be found.
+ public IEnumerable GetAllLocalizedStringResourcesForTextId(string languageFormat, uint textId)
+ {
+ return GetLocalizedTextDB(languageFormat).GetAllResourcesForTextId(textId);
+ }
+
+ ///
+ /// Returns the list of LocalizedStringResource in which the given text id can be found by default.
+ ///
+ ///
+ /// The text id to look for.
+ /// All resources in which the text id can be found by default.
+ public IEnumerable GetDefaultLocalizedStringResourcesForTextId(string languageFormat, uint textId)
{
- if (!strings.ContainsKey(id))
+ return GetLocalizedTextDB(languageFormat).GetDefaultResourcesForTextId(textId);
+ }
+
+ ///
+ /// Returns the list of LocalizedStringResource in which the given text id can be found due to a mod.
+ ///
+ ///
+ /// The text id to look for.
+ /// All resources in which the text id can be found due to a mod.
+ public IEnumerable GetAddedLocalizedStringResourcesForTextId(string languageFormat, uint textId)
+ {
+ return GetLocalizedTextDB(languageFormat).GetAddedResourcesForTextId(textId);
+ }
+
+ ///
+ /// Returns the names of all found resources
+ ///
+ ///
+ ///
+ public IEnumerable GetAllResourceNames(string languageFormat)
+ {
+ return GetLocalizedTextDB(languageFormat).GetAllResourceNames();
+ }
+
+ ///
+ /// Sets a text into a single resource
+ ///
+ ///
+ ///
+ ///
+ ///
+ public void SetText(string languageFormat, IEnumerable resourceNames, uint textId, string text)
+ {
+
+ LanguageTextsDB localizedDB = GetLocalizedTextDB(languageFormat);
+ foreach (string resourceName in resourceNames)
{
- if (id == 0)
- return "";
- return string.Format("Invalid StringId: {0}", id.ToString("X8"));
+ localizedDB.SetText(resourceName, textId, text);
}
- return strings[id];
+
+ localizedDB.UpdateTextCache(textId, text);
}
- public string GetString(string stringId)
+ ///
+ /// Removes the given text with the given id from the given resources for the given language.
+ ///
+ ///
+ ///
+ ///
+ public void RemoveText(string languageFormat, IEnumerable resourceNames, uint textId)
{
- throw new NotImplementedException();
+ LanguageTextsDB localizedDB = GetLocalizedTextDB(languageFormat);
+ foreach (string resourceName in resourceNames)
+ {
+ localizedDB.RemoveText(resourceName, textId);
+ }
+
+ localizedDB.RemoveTextFromCache(textId);
}
+ public void RevertText(string languageFormat, uint textId)
+ {
+ LanguageTextsDB localizedDB = GetLocalizedTextDB(languageFormat);
+ localizedDB.RevertText(textId);
+ }
+
+ public IEnumerable GellAllLanguages()
+ {
+ return new List(m_languageLocalizationBundles.Keys);
+ }
+
+ // basically identical to SetText, this method was added in the 1.06 beta interface
public void SetString(uint id, string value)
{
- throw new NotImplementedException();
+ LanguageTextsDB localizedDB = GetLocalizedTextDB(DefaultLanguage);
+
+ IEnumerable allTextResources = localizedDB.GetAllResourcesForTextId(id);
+ IEnumerable textResourceNames = allTextResources.Select(resource => resource.Name);
+
+ SetText(DefaultLanguage, textResourceNames, id, value);
}
+ // // Basically identical to SetText, this method was added in the 1.06 beta interface
public void SetString(string id, string value)
{
- throw new NotImplementedException();
+ bool canRead = uint.TryParse(id, NumberStyles.HexNumber, null, out uint textId);
+ if (canRead)
+ {
+ SetString(textId, value);
+ }
+
+ App.Logger.LogError("Cannot read given textId <{0}>", id);
}
+ // // Basically identical to RevertText, this method was added in the 1.06 beta interface
public void RevertString(uint id)
{
- throw new NotImplementedException();
+ RevertText(DefaultLanguage, id);
}
+ // Returns whether the text with the given id was altered.
+ // Implements the interface method added in 1.0.6beta
public bool isStringEdited(uint id)
{
+ LanguageTextsDB localizedDB = GetLocalizedTextDB(DefaultLanguage);
+
+ IEnumerable modifiedTextsIds = localizedDB.GetAllModifiedTextsIds();
+ foreach (uint textId in modifiedTextsIds)
+ {
+ if (textId == id)
+ {
+ return true;
+ }
+ }
+
return false;
}
+ ///
+ /// Opens a window to add strings to the localized string database.
+ ///
+ /// Note This method came with the 1.06.beta1 and i feel really uncomfortable displaying an edit dialog directly from what is supposed to be abackend class >_<
public void AddStringWindow()
{
- throw new NotImplementedException();
+
+ AddEditWindow editWindow = new AddEditWindow(this, DefaultLanguage)
+ {
+ Owner = Application.Current.MainWindow
+ };
+ editWindow.Init(0);
+ _ = editWindow.ShowDialog();
}
+ // This method came with 1.06.beta1, and i still believe bulk operations to be more a risk of breaking texts than working properly
+ // - or at least my implementation of that function would be ;D
+ // But now sequencial replacements should work somewhat decently
public void BulkReplaceWindow()
{
- throw new NotImplementedException();
+ List textsWithId = new List();
+
+ List textIds = GetAllTextIds(DefaultLanguage).ToList();
+ textIds.Sort();
+
+ foreach (uint textId in textIds)
+ {
+ string text = GetText(DefaultLanguage, textId);
+ textsWithId.Add(textId.ToString("X8") + " - " + text);
+ }
+
+ ReplaceWindow replaceWindow = new ReplaceWindow(this, DefaultLanguage, textsWithId)
+ {
+ Owner = Application.Current.MainWindow
+ };
+ replaceWindow.ShowDialog();
+ }
+
+ ///
+ /// Retrieves a collection of string IDs that were modified from the localized string database.
+ /// This method came into the interface in 1.06.beta1, and is virtually identical to GetAllModifiedTextsIds
+ ///
+ /// A collection of string IDs, or an empty collection if no modified strings exist.
+ public IEnumerable EnumerateModifiedStrings()
+ {
+ return GetAllModifiedTextsIds(DefaultLanguage);
}
- private void LoadLocalizedStringConfiguration(string type)
+ public IEnumerable GetAllResourceNamesWithDeclinatedAdjectives(string languageFormat)
{
- foreach (EbxAssetEntry entry in App.AssetManager.EnumerateEbx(type))
+ LanguageTextsDB textDb = GetLocalizedTextDB(languageFormat);
+ return textDb.GetAllResourceNamesWithDeclinatedAdjectives();
+ }
+
+ public IEnumerable GetAllResourceNamesWithModifiedDeclinatedAdjectives(string languageFormat)
+ {
+ LanguageTextsDB textDb = GetLocalizedTextDB(languageFormat);
+ return textDb.GetAllResourceNamesWithModifiedDeclinatedAdjectives();
+ }
+
+ public IEnumerable GetAllDeclinatedAdjectiveIdsFromResource(string languageFormat, string resourceName)
+ {
+ LanguageTextsDB textDb = GetLocalizedTextDB(languageFormat);
+ return textDb.GetAllDeclinatedAdjectiveIdsFromResource(resourceName);
+ }
+
+ public IEnumerable GetModifiedDeclinatedAdjectiveIdsFromResource(string languageFormat, string resourceName)
+ {
+ LanguageTextsDB textDb = GetLocalizedTextDB(languageFormat);
+ return textDb.GetModifiedDeclinatedAdjectiveIdsFromResource(resourceName);
+ }
+
+ public List GetDeclinatedAdjectives(string languageFormat, string resourceName, uint adjectiveId)
+ {
+ LanguageTextsDB textDb = GetLocalizedTextDB(languageFormat);
+ return textDb.GetDeclinatedAdjectives(resourceName, adjectiveId);
+ }
+
+ public void SetDeclinatedAdjectve(string languageFormat, string resourceName, uint adjectiveId, List aAdjectives)
+ {
+ LanguageTextsDB textDb = GetLocalizedTextDB(languageFormat);
+ textDb.SetDeclinatedAdjectve(resourceName, adjectiveId, aAdjectives);
+ }
+
+ public void RevertDeclinatedAdjective(string languageFormat, string resourceName, uint adjectiveId)
+ {
+ LanguageTextsDB textDb = GetLocalizedTextDB(languageFormat);
+ textDb.RevertDeclinatedAdjective(resourceName, adjectiveId);
+ }
+
+ public IEnumerable GetAllTextIdsFromResource(string languageFormat, string resourceName)
+ {
+ LanguageTextsDB textDb = GetLocalizedTextDB(languageFormat);
+ return textDb.GetAllTextIdsFromResource(resourceName);
+ }
+
+ public IEnumerable GetAllModifiedTextIdsFromResource(string languageFormat, string resourceName)
+ {
+ LanguageTextsDB textDb = GetLocalizedTextDB(languageFormat);
+ return textDb.GetAllModifiedTextIdsFromResource(resourceName);
+ }
+
+ ///
+ /// Fills the language dictionary with all available languages and their bundles.
+ ///
+ /// Sorted Dictionary of LangugeFormat names and their text super bundles paths.
+ private static SortedDictionary> GetLanguageDictionary()
+ {
+
+ var languagesRepository = new SortedDictionary>();
+
+ // There is no need to also search for 'LocalizedStringPatchTranslationsConfiguration', these are also found via their base type
+ foreach (EbxAssetEntry entry in App.AssetManager.EnumerateEbx("LocalizedStringTranslationsConfiguration"))
{
// read localization config
dynamic localizationAsset = App.AssetManager.GetEbx(entry).RootObject;
- // iterate thru language to bundle lists
- foreach (dynamic languageBundleList in localizationAsset.LanguagesToBundlesList)
+ // iterate through language to bundle lists
+ foreach (dynamic languageBundleListEntry in localizationAsset.LanguagesToBundlesList)
{
- if (languageBundleList.Language.ToString().Equals("LanguageFormat_English"))
+ string languageName = languageBundleListEntry.Language.ToString();
+ HashSet bundleNames;
+ if (languagesRepository.ContainsKey(languageName))
+ {
+ bundleNames = languagesRepository[languageName];
+ }
+ else
{
- foreach (string bundlePath in languageBundleList.BundlePaths)
- {
- string bundleFullPath = "win32/" + bundlePath.ToLower();
- foreach (ResAssetEntry resEntry in App.AssetManager.EnumerateRes(resType: (uint)ResourceType.LocalizedStringResource))
- {
- bool bFound = false;
- foreach (int bindex in resEntry.EnumerateBundles())
- {
- BundleEntry be = App.AssetManager.GetBundleEntry(bindex);
- if (be.Name.StartsWith(bundleFullPath, StringComparison.OrdinalIgnoreCase))
- {
- bFound = true;
- break;
- }
- }
-
- if (bFound)
- {
- LocalizedStringResource resource = App.AssetManager.GetResAs(resEntry);
- if (resource != null)
- {
- foreach (KeyValuePair kvp in resource.Strings)
- {
- if (!strings.ContainsKey(kvp.Key))
- {
- strings.Add(kvp.Key, kvp.Value);
- }
- }
- }
- }
- }
- }
+ bundleNames = new HashSet();
+ languagesRepository[languageName] = bundleNames;
+ }
+
+ foreach (string bundlepath in languageBundleListEntry.BundlePaths)
+ {
+ bundleNames.Add(bundlepath);
}
}
}
+
+ return languagesRepository;
}
}
}
diff --git a/Plugins/BiowareLocalizationPlugin/Controls/AddEditWindow.xaml b/Plugins/BiowareLocalizationPlugin/Controls/AddEditWindow.xaml
new file mode 100644
index 000000000..179fbe318
--- /dev/null
+++ b/Plugins/BiowareLocalizationPlugin/Controls/AddEditWindow.xaml
@@ -0,0 +1,114 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Plugins/BiowareLocalizationPlugin/Controls/AddEditWindow.xaml.cs b/Plugins/BiowareLocalizationPlugin/Controls/AddEditWindow.xaml.cs
new file mode 100644
index 000000000..dcfd4e6e9
--- /dev/null
+++ b/Plugins/BiowareLocalizationPlugin/Controls/AddEditWindow.xaml.cs
@@ -0,0 +1,213 @@
+using BiowareLocalizationPlugin.LocalizedResources;
+using Frosty.Controls;
+using Frosty.Core;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+
+namespace BiowareLocalizationPlugin.Controls
+{
+
+ ///
+ /// Via this window the user can add or edit existing texts.
+ /// This is a positive logic only window, i.e., you can add texts to additional resource, but cannot deselect it again.
+ ///
+ [TemplatePart(Name = "textIdField", Type = typeof(TextBox))]
+ [TemplatePart(Name = "localizedTextBox", Type = typeof(TextBox))]
+ [TemplatePart(Name = "addedResourcesListBox", Type = typeof(ListBox))]
+ [TemplatePart(Name = "defaultResourcesListBox", Type = typeof(ListBox))]
+ [TemplatePart(Name = "saveButton", Type = typeof(Button))]
+ [TemplatePart(Name = "cancelButton", Type = typeof(Button))]
+ public partial class AddEditWindow : FrostyDockableWindow
+ {
+
+ ///
+ /// The save value tuple consiting of the text id, and text. This is only available after the save action!
+ ///
+ public Tuple SaveValue { get; private set; }
+
+ private readonly string m_selectedLanguageFormat;
+ private readonly BiowareLocalizedStringDatabase m_stringDb;
+
+ ///
+ /// List to keep track of resources where a text id was removed from.
+ ///
+ private readonly List m_removedResources = new List();
+
+ public AddEditWindow(BiowareLocalizedStringDatabase inStringDb, string inLanguageFormat)
+ {
+ InitializeComponent();
+
+ m_selectedLanguageFormat = inLanguageFormat;
+ m_stringDb = inStringDb;
+ }
+
+ ///
+ /// Initializes the window with the currently selected textid and text.
+ ///
+ ///
+ public void Init(uint textId)
+ {
+
+ if (textId != 0)
+ {
+ textIdField.Text = textId.ToString("X8");
+ }
+ UpdateData(textId);
+ }
+
+ private void Update(object sender, RoutedEventArgs e)
+ {
+ uint textId = ReadTextId();
+ UpdateData(textId);
+ }
+
+ ///
+ /// Updates the listed data
+ ///
+ private void UpdateData(uint textId)
+ {
+
+ if (textId == 0)
+ {
+ // revert;
+ localizedTextBox.Text = "";
+ DeselectAllResources();
+ }
+ else
+ {
+ localizedTextBox.Text = m_stringDb.FindText(m_selectedLanguageFormat, textId);
+ DeselectAllResources();
+
+ IEnumerable addedResources = m_stringDb.GetAddedLocalizedStringResourcesForTextId(m_selectedLanguageFormat, textId);
+ foreach (LocalizedStringResource res in addedResources)
+ {
+ addedResourcesListBox.Items.Add(res.Name);
+ }
+
+ IEnumerable defaultResources = m_stringDb.GetDefaultLocalizedStringResourcesForTextId(m_selectedLanguageFormat, textId);
+ ListBoxUtils.SortListIntoListBox(defaultResources.Select(r => r.Name), defaultResourcesListBox);
+ }
+ }
+
+ private void DeselectAllResources()
+ {
+ defaultResourcesListBox.Items.Clear();
+ addedResourcesListBox.Items.Clear();
+
+ m_removedResources.Clear();
+ }
+
+ private void Save(object sender, RoutedEventArgs e)
+ {
+ uint textId = ReadTextId();
+
+ if (textId != 0)
+ {
+
+ // TODO move all this text handling into a dedicated handler or something.
+ // This is currently all over the place >_<
+
+ m_stringDb.RemoveText(m_selectedLanguageFormat, m_removedResources, textId);
+
+ List resources = new List();
+ foreach (string resourceName in defaultResourcesListBox.Items)
+ {
+ resources.Add(resourceName);
+ }
+ foreach (string resourceName in addedResourcesListBox.Items)
+ {
+ resources.Add(resourceName);
+ }
+
+ string text = localizedTextBox.Text;
+ m_stringDb.SetText(m_selectedLanguageFormat, resources, textId, text);
+
+ SaveValue = Tuple.Create(textId, text);
+
+ DialogResult = true;
+ Close();
+ }
+ }
+
+ ///
+ /// Closes the dialog without saving
+ ///
+ ///
+ ///
+ private void Close(object sender, RoutedEventArgs e)
+ {
+ DialogResult = false;
+ SaveValue = null;
+ Close();
+ }
+
+ ///
+ /// Reads the set hexadecimal text id and sets the lokal variable with the parsed result.
+ ///
+ /// _textId
+ private uint ReadTextId()
+ {
+ string text = textIdField.Text;
+ bool canRead = uint.TryParse(text, NumberStyles.HexNumber, null, out uint textId);
+ if (canRead)
+ {
+ return textId;
+ }
+
+ textIdField.Text = "";
+ App.Logger.LogWarning("Bad Input!");
+ return 0;
+ }
+
+ ///
+ /// Shows a popup dialog to select one or more resources where to add the current text into.
+ ///
+ ///
+ ///
+ private void AddResources(object sender, RoutedEventArgs e)
+ {
+
+ IEnumerable selectableResources = m_stringDb.GetAllResourceNames(m_selectedLanguageFormat)
+ .Where(r => !defaultResourcesListBox.Items.Contains(r)
+ && !addedResourcesListBox.Items.Contains(r));
+
+ ResourceSelectionWindow selectionDialog = new ResourceSelectionWindow(selectableResources);
+ bool? saved = selectionDialog.ShowDialog();
+
+ if (saved != null && saved.Value)
+ {
+ foreach (string resourceName in selectionDialog.SelectedResources)
+ {
+ addedResourcesListBox.Items.Add(resourceName);
+ }
+ }
+ }
+
+ private void RemoveResources(object sender, RoutedEventArgs e)
+ {
+
+ List selectedToRemove = addedResourcesListBox.SelectedItems.OfType().ToList();
+ foreach (string itemToRemove in selectedToRemove)
+ {
+ addedResourcesListBox.Items.Remove(itemToRemove);
+ }
+
+ m_removedResources.AddRange(selectedToRemove);
+ }
+
+ ///
+ /// Called to copy the selected values of either listbox to the clipboard.
+ ///
+ ///
+ ///
+ private void CopySelectionToClipboard(object sender, ExecutedRoutedEventArgs e)
+ {
+ ListBoxUtils.CopySelectionToClipboard(sender, e);
+ }
+ }
+}
diff --git a/Plugins/BiowareLocalizationPlugin/Controls/BiowareLocalizedStringEditor.cs b/Plugins/BiowareLocalizationPlugin/Controls/BiowareLocalizedStringEditor.cs
new file mode 100644
index 000000000..4276b2720
--- /dev/null
+++ b/Plugins/BiowareLocalizationPlugin/Controls/BiowareLocalizedStringEditor.cs
@@ -0,0 +1,942 @@
+using BiowareLocalizationPlugin.ExportImport;
+using Frosty.Core;
+using Frosty.Core.Controls;
+using Frosty.Core.Windows;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+
+
+namespace BiowareLocalizationPlugin.Controls
+{
+
+ ///
+ /// This is basically a copy of the FrostyLocalizedStringViewer, with some added functionality
+ ///
+ [TemplatePart(Name = PART_LocalizedString, Type = typeof(TextBox))]
+ [TemplatePart(Name = PART_StringIdList, Type = typeof(ListBox))]
+ [TemplatePart(Name = PART_ToggleDisplayTextsOrAdjectives, Type = typeof(Button))]
+ [TemplatePart(Name = PART_hexSearchCB, Type = typeof(CheckBox))]
+ [TemplatePart(Name = PART_Searchfield, Type = typeof(TextBox))]
+ [TemplatePart(Name = PART_SearchButton, Type = typeof(Button))]
+ [TemplatePart(Name = PART_SearchTextButton, Type = typeof(Button))]
+ [TemplatePart(Name = PART_ModifiedOnlyCB, Type = typeof(CheckBox))]
+ [TemplatePart(Name = PART_UpdateTextIdFieldCB, Type = typeof(CheckBox))]
+ [TemplatePart(Name = PART_ShowTextInfo, Type = typeof(Button))]
+ [TemplatePart(Name = PART_AddEdit, Type = typeof(Button))]
+ [TemplatePart(Name = PART_Replace, Type = typeof(Button))]
+ [TemplatePart(Name = PART_Remove, Type = typeof(Button))]
+ [TemplatePart(Name = PART_LanguageSelector, Type = typeof(ComboBox))]
+ [TemplatePart(Name = PART_RefreshButton, Type = typeof(Button))]
+ [TemplatePart(Name = PART_Export, Type = typeof(Button))]
+ [TemplatePart(Name = PART_Import, Type = typeof(Button))]
+ [TemplatePart(Name = PART_ResourceSelector, Type = typeof(ComboBox))]
+ class BiowareLocalizedStringEditor : FrostyBaseEditor
+ {
+ #region constants and variables
+ private const string PART_LocalizedString = "PART_LocalizedString";
+
+ private const string PART_StringIdList = "PART_StringIdList";
+
+ private const string PART_ToggleDisplayTextsOrAdjectives = "PART_ToggleDisplayTextsOrAdjectives";
+ private const string PART_hexSearchCB = "PART_hexSearchCB";
+ private const string PART_Searchfield = "PART_Searchfield";
+ private const string PART_SearchButton = "PART_SearchButton";
+ private const string PART_SearchTextButton = "PART_SearchTextButton";
+ private const string PART_ModifiedOnlyCB = "PART_ModifiedOnlyCB";
+ private const string PART_UpdateTextIdFieldCB = "PART_UpdateTextIdFieldCB";
+
+ private const string PART_ShowTextInfo = "PART_ShowTextInfo";
+
+ private const string PART_AddEdit = "PART_AddEdit";
+ private const string PART_Replace = "PART_Replace";
+ private const string PART_Remove = "PART_Remove";
+
+ private const string PART_LanguageSelector = "PART_LanguageSelector";
+ private const string PART_RefreshButton = "PART_RefreshButton";
+
+ private const string PART_Export = "PART_Export";
+ private const string PART_Import = "PART_Import";
+
+ private const string PART_ResourceSelector = "PART_ResourceSelector";
+
+ //##############################################################################
+
+ private const string m_toggleDisplayButtonTextsString = "Search Text with ID:";
+ private const string m_toggleDisplayButtonAdjectiveString = "Search Adjective with ID:";
+
+ private const string m_toggleDisplayButtonTooltipTextsString = "Searches for text entries from the default text id space\r\n"
+ + "Use this toggle to show only declinated adjectives used for names of crafted items in DA:I";
+
+ private const string m_toggleDisplayButtonTooltipAdjectiveString = "Searches for declinated adjectives used for generating the names of crafted items in DA:I\r\n"
+ + "Use this toggle to show only the normal texts used in both DA:I and ME:A";
+
+ private const string m_SHOW_ALL_RESOURCES = "";
+
+ //##############################################################################
+
+ ///
+ /// The text db instance, stored as variable for convenience
+ ///
+ private readonly BiowareLocalizedStringDatabase m_textDB;
+
+ ///
+ /// handler to support closing child windows if this editor is closed.
+ ///
+ private readonly ClosingHandler m_closingHandler;
+
+ private Button m_toggleTextsOrAdjectivesButton;
+
+ private CheckBox m_searchHexIdCB;
+
+ private TextBox m_localizedStringTb;
+
+ private ListBox m_stringIdListBox;
+
+ private TextBox m_searchfieldTb;
+
+ private CheckBox m_modifiedOnlyCB;
+
+ private CheckBox m_updateTextIdFieldCB;
+
+ private Button m_textInfoBt;
+
+ private Button m_replaceButton;
+
+ private Button m_removeButton;
+
+ private ComboBox m_languageSelectorCb;
+
+ private List m_textIdsList = new List();
+
+ private string m_selectedLanguageFormat;
+
+ private bool m_isFirstTimeInitialization = true;
+
+ private ComboBox m_resourceSelectorCb;
+
+ ///
+ /// Enum for the types of strings to display.
+ /// These can be the texts as defined in the text id block, or the declinated adjectives used for the generated names of crafted items in DA:I.
+ /// Maybe even a combination of those, currently the text ids and adjective ids do not overlapp afaik.
+ ///
+ private enum DisplayType
+ {
+
+ ///
+ /// Show only the texts - this is the defaul behaviour.
+ ///
+ SHOW_TEXTS,
+
+ ///
+ /// Show only the adjectives - this is useless for ME:A and very likely not often used for DA:I
+ ///
+ SHOW_DECLINATED_ADJECTIVES
+ };
+
+ ///
+ /// What to display at this time
+ ///
+ private DisplayType m_displayType = DisplayType.SHOW_TEXTS;
+
+ ///
+ /// The last selected resource - Note that this can be null!
+ ///
+ private string m_selectedResource = m_SHOW_ALL_RESOURCES;
+
+ //##############################################################################
+ #endregion constants and variables
+
+ #region initialization
+
+ static BiowareLocalizedStringEditor()
+ {
+ DefaultStyleKeyProperty.OverrideMetadata(typeof(BiowareLocalizedStringEditor), new FrameworkPropertyMetadata(typeof(BiowareLocalizedStringEditor)));
+ }
+
+ public BiowareLocalizedStringEditor(BiowareLocalizedStringDatabase inTextDb)
+ {
+ m_textDB = inTextDb;
+ m_selectedLanguageFormat = inTextDb.DefaultLanguage;
+ m_closingHandler = new ClosingHandler();
+ }
+
+ public override void OnApplyTemplate()
+ {
+ base.OnApplyTemplate();
+
+ m_stringIdListBox = GetTemplateChild(PART_StringIdList) as ListBox;
+ m_stringIdListBox.SelectionChanged += StringIdListbox_SelectionChanged;
+
+ m_localizedStringTb = GetTemplateChild(PART_LocalizedString) as TextBox;
+
+ m_toggleTextsOrAdjectivesButton = GetTemplateChild(PART_ToggleDisplayTextsOrAdjectives) as Button;
+ m_toggleTextsOrAdjectivesButton.Content = m_toggleDisplayButtonTextsString;
+ m_toggleTextsOrAdjectivesButton.ToolTip = m_toggleDisplayButtonTooltipTextsString;
+ m_toggleTextsOrAdjectivesButton.Click += ToggleTextsOrAdjectives;
+
+ m_searchHexIdCB = GetTemplateChild(PART_hexSearchCB) as CheckBox;
+ m_searchHexIdCB.Checked += SearchFieldFormatChangedToHex;
+ m_searchHexIdCB.Unchecked += SearchFieldFormatChangedToDecimal;
+
+ m_searchfieldTb = GetTemplateChild(PART_Searchfield) as TextBox;
+ m_searchfieldTb.PreviewKeyDown += SearchFieldActualized;
+ Button btSearchButton = GetTemplateChild(PART_SearchButton) as Button;
+ btSearchButton.Click += SearchButtonClicked;
+
+ Button btSearchTextButton = GetTemplateChild(PART_SearchTextButton) as Button;
+ btSearchTextButton.Click += ShowSearchDialog;
+
+ m_modifiedOnlyCB = GetTemplateChild(PART_ModifiedOnlyCB) as CheckBox;
+ m_modifiedOnlyCB.Click += ReLoadStrings;
+
+ m_updateTextIdFieldCB = GetTemplateChild(PART_UpdateTextIdFieldCB) as CheckBox;
+
+ m_textInfoBt = GetTemplateChild(PART_ShowTextInfo) as Button;
+ m_textInfoBt.IsEnabled = false; // initially disabled until a text is selected
+ m_textInfoBt.Click += ShowTextInfo;
+
+ Button addButton = GetTemplateChild(PART_AddEdit) as Button;
+ addButton.Click += ShowAddEditWindow;
+
+ m_replaceButton = GetTemplateChild(PART_Replace) as Button;
+ m_replaceButton.Click += ShowReplaceWindow;
+
+ m_removeButton = GetTemplateChild(PART_Remove) as Button;
+ m_removeButton.IsEnabled = false; // initially disabled until a text is selected
+ m_removeButton.Click += Remove;
+
+ Button refreshButton = GetTemplateChild(PART_RefreshButton) as Button;
+ refreshButton.Click += ReLoadStrings;
+
+ Button exportButton = GetTemplateChild(PART_Export) as Button;
+ exportButton.Click += Export;
+
+ Button importButton = GetTemplateChild(PART_Import) as Button;
+ importButton.Click += Import;
+
+ // listeners are active form the beginning, so we have to keep a certain order in here for the language and resource listeners.
+ m_languageSelectorCb = GetTemplateChild(PART_LanguageSelector) as ComboBox;
+ m_languageSelectorCb.ItemsSource = m_textDB.GellAllLanguages();
+ m_languageSelectorCb.SelectedItem = m_selectedLanguageFormat;
+ m_languageSelectorCb.SelectionChanged += SelectLanguage;
+
+ m_resourceSelectorCb = GetTemplateChild(PART_ResourceSelector) as ComboBox;
+ SetSelectableResources();
+ m_resourceSelectorCb.SelectionChanged += SelectResource;
+
+ Loaded += LoadFirstTime;
+
+ }
+
+ #endregion initialization
+
+ #region data loading
+ private void LoadFirstTime(object sender, RoutedEventArgs e)
+ {
+ if (m_isFirstTimeInitialization)
+ {
+ LoadStrings(sender, e);
+ m_isFirstTimeInitialization = false;
+ }
+ }
+
+ private void ReLoadStrings(object sender, RoutedEventArgs e)
+ {
+ m_textIdsList.Clear();
+ m_stringIdListBox.Items.Clear();
+
+ LoadStrings(sender, e);
+ }
+
+ private void LoadStrings(object sender, RoutedEventArgs e)
+ {
+
+ bool? nullableModifiedOnly = m_modifiedOnlyCB.IsChecked;
+ bool modifiedOnly = nullableModifiedOnly.HasValue && nullableModifiedOnly.Value;
+
+ if (m_selectedResource == null)
+ {
+ m_textIdsList = new List();
+ return;
+ }
+
+ switch (m_displayType)
+ {
+ case DisplayType.SHOW_TEXTS:
+ LoadTexts0(modifiedOnly);
+ break;
+ case DisplayType.SHOW_DECLINATED_ADJECTIVES:
+ LoadAdjectives0(modifiedOnly);
+ break;
+ default:
+ throw new InvalidEnumArgumentException("Unknown DisplayType: " + m_displayType);
+ }
+
+ if (m_textIdsList.Count == 0)
+ {
+ return;
+ }
+ m_stringIdListBox.ScrollIntoView(m_stringIdListBox.Items[0]);
+ }
+
+ private void LoadTexts0(bool modifiedOnly)
+ {
+ bool showTextsFromAllResources = m_SHOW_ALL_RESOURCES.Equals(m_selectedResource);
+ FrostyTaskWindow.Show("Loading texts", "", (task) =>
+ {
+ m_textIdsList = LoadTextIds(modifiedOnly, showTextsFromAllResources);
+ });
+
+ m_textIdsList.Sort();
+
+ foreach (uint textId in m_textIdsList)
+ {
+ m_stringIdListBox.Items.Add(textId.ToString("X8") + " - " + m_textDB.GetText(m_selectedLanguageFormat, textId));
+ }
+ }
+
+ private List LoadTextIds(bool modifiedOnly, bool showTextsFromAllResources)
+ {
+
+ List textIds;
+
+ if (!modifiedOnly && showTextsFromAllResources)
+ {
+ textIds = m_textDB.GetAllTextIds(m_selectedLanguageFormat).ToList();
+ }
+ else if (modifiedOnly && showTextsFromAllResources)
+ {
+ textIds = m_textDB.GetAllModifiedTextsIds(m_selectedLanguageFormat).ToList();
+ }
+ else if (modifiedOnly && !showTextsFromAllResources)
+ {
+ textIds = m_textDB.GetAllModifiedTextIdsFromResource(m_selectedLanguageFormat, m_selectedResource).ToList();
+ }
+ else //if( !modifiedOnly && !showTextsFromAllResources)
+ {
+ textIds = m_textDB.GetAllTextIdsFromResource(m_selectedLanguageFormat, m_selectedResource).ToList();
+ }
+
+ return textIds;
+ }
+
+ private void LoadAdjectives0(bool modifiedOnly)
+ {
+
+ FrostyTaskWindow.Show("Loading adjectives", "", (task) =>
+ {
+ m_textIdsList = LoadAdjectiveIds(modifiedOnly);
+ });
+
+ m_textIdsList.Sort();
+
+ foreach (uint adjectiveId in m_textIdsList)
+ {
+ List adjectives = m_textDB.GetDeclinatedAdjectives(m_selectedLanguageFormat, m_selectedResource, adjectiveId);
+
+ string firstAdjective = adjectives.Count > 0 ? adjectives[0] : "";
+
+ m_stringIdListBox.Items.Add(adjectiveId.ToString("X8") + " - " + firstAdjective);
+ }
+ }
+
+ private List LoadAdjectiveIds(bool modifiedOnly)
+ {
+ if (modifiedOnly)
+ {
+ return m_textDB.GetModifiedDeclinatedAdjectiveIdsFromResource(m_selectedLanguageFormat, m_selectedResource).ToList();
+ }
+ else
+ {
+ return m_textDB.GetAllDeclinatedAdjectiveIdsFromResource(m_selectedLanguageFormat, m_selectedResource).ToList();
+ }
+ }
+
+ ///
+ /// Sets the list of currently applicable resources for the selected language and displaytype in the resource selector.
+ ///
+ private void SetSelectableResources()
+ {
+
+ m_resourceSelectorCb.Items.Clear();
+ List resourceNames = GetApplicableResources();
+
+ foreach (string resourceName in resourceNames)
+ {
+ m_resourceSelectorCb.Items.Add(resourceName);
+ }
+
+ if (resourceNames.Count > 0)
+ {
+ m_resourceSelectorCb.SelectedItem = m_resourceSelectorCb.Items[0];
+ }
+ else
+ {
+ m_resourceSelectorCb.SelectedItem = null;
+ }
+ }
+
+ ///
+ /// Returns the list or resources that are selectable for the currently selected language and display type.
+ ///
+ ///
+ private List GetApplicableResources()
+ {
+
+ if (m_selectedLanguageFormat == null)
+ {
+ // we are either still in setup or something went wrong...
+ return new List();
+ }
+
+ List resourceList = new List();
+
+ switch (m_displayType)
+ {
+ case DisplayType.SHOW_DECLINATED_ADJECTIVES:
+ resourceList.AddRange(m_textDB.GetAllResourceNamesWithDeclinatedAdjectives(m_selectedLanguageFormat));
+ break;
+
+ case DisplayType.SHOW_TEXTS:
+ default:
+ resourceList.Add(m_SHOW_ALL_RESOURCES);
+ resourceList.AddRange(m_textDB.GetAllResourceNames(m_selectedLanguageFormat));
+ break;
+ }
+
+ return resourceList;
+ }
+
+ #endregion data loading
+
+ ///
+ /// Returns the id of the currently selected text, or zero 0, if no text is currently selected.
+ ///
+ ///
+ private uint GetCurrentStringId()
+ {
+ int selectedIndex = m_stringIdListBox.SelectedIndex;
+ return selectedIndex >= 0 && selectedIndex < m_textIdsList.Count ? m_textIdsList[selectedIndex] : 0;
+ }
+
+ #region string selection listeners
+
+ private void StringIdListbox_SelectionChanged(object sender, SelectionChangedEventArgs e)
+ {
+ uint selectedTextId = GetCurrentStringId();
+ PopulateLocalizedString(selectedTextId);
+
+ bool isSelectionValid = e.AddedItems.Count > 0;
+ bool isTextSelection = DisplayType.SHOW_TEXTS == m_displayType;
+
+ m_textInfoBt.IsEnabled = isSelectionValid && isTextSelection;
+ m_removeButton.IsEnabled = isSelectionValid;
+
+ if (isSelectionValid && m_updateTextIdFieldCB.IsChecked == true)
+ {
+ SetTextIdInSearchField(selectedTextId);
+ }
+ }
+
+ private void SetTextIdInSearchField(uint textId)
+ {
+ string textIdFormat = (m_searchHexIdCB.IsChecked == true) ? "X8" : "D";
+ m_searchfieldTb.Text = textId.ToString(textIdFormat);
+ }
+
+ private void PopulateLocalizedString(uint textId)
+ {
+
+ string textToShow;
+ switch (m_displayType)
+ {
+ case DisplayType.SHOW_TEXTS:
+ textToShow = m_textDB.GetText(m_selectedLanguageFormat, textId);
+ break;
+
+ case DisplayType.SHOW_DECLINATED_ADJECTIVES:
+ textToShow = LoadDeclinatedAdjectiveToShow(textId);
+ break;
+
+ default:
+ throw new InvalidEnumArgumentException("Unknown DisplayType: " + m_displayType);
+ }
+
+ m_localizedStringTb.Text = textToShow;
+ }
+
+ private string LoadDeclinatedAdjectiveToShow(uint adjectiveId)
+ {
+
+ if (m_selectedResource == null)
+ {
+ return "";
+ }
+
+ List declinations = m_textDB.GetDeclinatedAdjectives(m_selectedLanguageFormat, m_selectedResource, adjectiveId);
+
+ StringBuilder displayBuilder = new StringBuilder();
+ foreach (string declination in declinations)
+ {
+ displayBuilder.AppendLine(declination);
+ }
+
+ return displayBuilder.ToString();
+ }
+
+ #endregion string selection listeners
+
+ #region top search bar button listeners
+
+ void SearchButtonClicked(object sender, RoutedEventArgs e)
+ {
+ DoSearch();
+ }
+
+ private void SearchFieldActualized(object sender, KeyEventArgs e)
+ {
+ if (e.Key == Key.Return)
+ {
+ DoSearch();
+ }
+ }
+
+ private void SearchFieldFormatChangedToHex(object sender, RoutedEventArgs e)
+ {
+ GetUpdateFromTextField(NumberStyles.Number, textId => SetTextIdInSearchField(textId));
+ }
+
+ private void SearchFieldFormatChangedToDecimal(object sender, RoutedEventArgs e)
+ {
+ GetUpdateFromTextField(NumberStyles.HexNumber, textId => SetTextIdInSearchField(textId));
+ }
+
+ ///
+ /// Toggles the button and the displayed texts or adjectives.
+ ///
+ ///
+ ///
+ private void ToggleTextsOrAdjectives(object sender, RoutedEventArgs e)
+ {
+ string buttonText;
+ string buttonTooltip;
+
+ switch (m_displayType)
+ {
+ case DisplayType.SHOW_TEXTS:
+ m_displayType = DisplayType.SHOW_DECLINATED_ADJECTIVES;
+ buttonText = m_toggleDisplayButtonAdjectiveString;
+ buttonTooltip = m_toggleDisplayButtonTooltipAdjectiveString;
+ m_replaceButton.IsEnabled = false;
+ break;
+
+ case DisplayType.SHOW_DECLINATED_ADJECTIVES:
+ default:
+ m_displayType = DisplayType.SHOW_TEXTS;
+ buttonText = m_toggleDisplayButtonTextsString;
+ buttonTooltip = m_toggleDisplayButtonTooltipTextsString;
+ m_replaceButton.IsEnabled = true;
+ break;
+ }
+
+ m_toggleTextsOrAdjectivesButton.Content = buttonText;
+ m_toggleTextsOrAdjectivesButton.ToolTip = buttonTooltip;
+
+ SetSelectableResources();
+ }
+
+ private void GetUpdateFromTextField(NumberStyles style, Action textIdAction)
+ {
+ string stringIdAsText = m_searchfieldTb.Text;
+
+ if (stringIdAsText == null || stringIdAsText.Length == 0)
+ {
+ return;
+ }
+
+ bool canRead = uint.TryParse(stringIdAsText, style, null, out uint textId);
+ if (canRead)
+ {
+ textIdAction(textId);
+ }
+ else
+ {
+ App.Logger.LogWarning("Bad Input! Cannot read <{0}> as {1} formatted number for text Id", stringIdAsText, style.ToString());
+ }
+ }
+
+
+ ///
+ /// Searches the list of string ids for the text id (assumed a hexadecimal value!) given in the search box.
+ ///
+ private void DoSearch()
+ {
+ NumberStyles style = (m_searchHexIdCB.IsChecked == true) ? NumberStyles.HexNumber : NumberStyles.Integer;
+ GetUpdateFromTextField(style, textId => SearchTextId(textId));
+ }
+
+ private void SearchTextId(uint textId)
+ {
+ int index = m_textIdsList.IndexOf(textId);
+ if (index < 0 && index < m_stringIdListBox.Items.Count)
+ {
+ m_stringIdListBox.UnselectAll();
+ return;
+ }
+
+ m_stringIdListBox.SelectedIndex = index;
+ m_stringIdListBox.ScrollIntoView(m_stringIdListBox.SelectedItem);
+
+ SetTextIdInSearchField(textId);
+ }
+
+ private void SelectLanguage(object sender, SelectionChangedEventArgs e)
+ {
+
+ string newLanguageFormat = (string)m_languageSelectorCb.SelectedItem;
+
+ if (!m_selectedLanguageFormat.Equals(newLanguageFormat))
+ {
+ m_selectedLanguageFormat = newLanguageFormat;
+
+ SetSelectableResources();
+ }
+ }
+
+ private void ShowSearchDialog(object sender, RoutedEventArgs e)
+ {
+
+ if (m_stringIdListBox != null && m_stringIdListBox.Items.Count > 0)
+ {
+ SearchFindWindow searchWindow = new SearchFindWindow(m_stringIdListBox);
+ searchWindow.Show();
+
+ m_closingHandler.AddChildWindow(searchWindow);
+ }
+ }
+
+ private void SelectResource(object sender, SelectionChangedEventArgs e)
+ {
+
+ string newResource = (string)m_resourceSelectorCb.SelectedItem;
+
+ bool changed = false;
+ if (m_selectedResource == null || newResource == null)
+ {
+ m_selectedResource = newResource;
+ changed = true;
+ }
+ else if (!m_selectedResource.Equals(newResource))
+ {
+ m_selectedResource = newResource;
+ changed = true;
+ }
+
+ if (changed)
+ {
+ ReLoadStrings(sender, e);
+ }
+ }
+
+ #endregion top search bar button listeners
+
+ #region operation buttons
+
+ ///
+ /// Opens another window that details extra information about the text that is not normally necessary to have.
+ ///
+ ///
+ ///
+ private void ShowTextInfo(object sender, RoutedEventArgs e)
+ {
+
+ if (DisplayType.SHOW_DECLINATED_ADJECTIVES == m_displayType)
+ {
+ App.Logger.Log("This function is currently not available for adjectives");
+ return;
+ }
+
+ uint stringId = GetCurrentStringId();
+
+ TextInfoWindow infoWindow = new TextInfoWindow
+ {
+ Owner = Application.Current.MainWindow
+ };
+ infoWindow.Init(m_selectedLanguageFormat, stringId, m_textDB);
+ infoWindow.Show();
+
+ m_closingHandler.AddChildWindow(infoWindow);
+ }
+
+ ///
+ /// Shows the edit window, taking the results into the currently displayed entries.
+ ///
+ ///
+ ///
+ private void ShowAddEditWindow(object sender, RoutedEventArgs e)
+ {
+
+ uint stringId = GetCurrentStringId();
+
+ switch (m_displayType)
+ {
+ case DisplayType.SHOW_TEXTS:
+ ShowTextEditWindow(stringId);
+ break;
+
+ case DisplayType.SHOW_DECLINATED_ADJECTIVES:
+ ShowAdjectiveEditWindow(stringId);
+ break;
+
+ default:
+ throw new InvalidEnumArgumentException("Unknown DisplayType: " + m_displayType);
+ }
+
+ }
+
+ private void ShowTextEditWindow(uint textId)
+ {
+ AddEditWindow editWindow = new AddEditWindow(m_textDB, m_selectedLanguageFormat)
+ {
+ Owner = Application.Current.MainWindow
+ };
+ editWindow.Init(textId);
+
+ bool? save = editWindow.ShowDialog();
+ if (save.HasValue && save.Value)
+ {
+ Tuple saveValue = editWindow.SaveValue;
+
+ // textId is not necessarily the stringId originally given to the dialog!
+ textId = saveValue.Item1;
+ string text = saveValue.Item2;
+
+ string entry = textId.ToString("X8") + " - " + text;
+
+ int entryIndex = m_textIdsList.IndexOf(textId);
+
+ if (entryIndex < 0)
+ {
+ m_stringIdListBox.Items.Add(entry);
+ m_textIdsList.Add(textId);
+ }
+ else
+ {
+ m_stringIdListBox.Items[entryIndex] = entry;
+ }
+
+ SearchTextId(textId);
+ }
+ }
+
+ private void ShowAdjectiveEditWindow(uint adjectiveId)
+ {
+ EditAdjectivesWindow editWindow = new EditAdjectivesWindow(m_textDB, m_selectedLanguageFormat, m_selectedResource)
+ {
+ Owner = Application.Current.MainWindow
+ };
+ editWindow.Init(adjectiveId);
+
+ bool? save = editWindow.ShowDialog();
+ if (save.HasValue && save.Value)
+ {
+
+ Tuple> saveValue = editWindow.SaveValue;
+
+ adjectiveId = saveValue.Item1;
+ List declinations = saveValue.Item2;
+
+ string firstDeclination = declinations.Count > 0 ? declinations[0] : "";
+
+ string entry = adjectiveId.ToString("X8") + " - " + firstDeclination;
+
+ int entryIndex = m_textIdsList.IndexOf(adjectiveId);
+
+ if (entryIndex < 0)
+ {
+ m_stringIdListBox.Items.Add(entry);
+ m_textIdsList.Add(adjectiveId);
+ }
+ else
+ {
+ m_stringIdListBox.Items[entryIndex] = entry;
+ }
+
+ SearchTextId(adjectiveId);
+ }
+ }
+
+ ///
+ /// Removes / Reverts the selected text.
+ ///
+ ///
+ ///
+ private void Remove(object sender, RoutedEventArgs e)
+ {
+
+ int index = m_stringIdListBox.SelectedIndex;
+
+ if (index < 0 || index >= m_textIdsList.Count)
+ {
+ // not sure how this should be possible...
+ App.Logger.LogWarning("Entered impossible state : Remove Operation did not complete!");
+ return;
+ }
+
+ uint selectedId = m_textIdsList[index];
+
+ bool entryStillExists;
+ switch (m_displayType)
+ {
+ case DisplayType.SHOW_TEXTS:
+ entryStillExists = RemoveText0(index, selectedId);
+ break;
+
+ case DisplayType.SHOW_DECLINATED_ADJECTIVES:
+ entryStillExists = RevertAdjective0(index, selectedId);
+ break;
+
+ default:
+ throw new InvalidEnumArgumentException("Unknown DisplayType: " + m_displayType);
+ }
+
+ if (!entryStillExists)
+ {
+ m_stringIdListBox.Items.RemoveAt(index);
+ m_textIdsList.RemoveAt(index);
+ }
+
+ SearchTextId(selectedId);
+ }
+
+ ///
+ /// Calls the appropriate methods to revert or delete a text entry. Returns whether or not a text of the given id still exists afterwards.
+ ///
+ ///
+ ///
+ /// true if a text of the id still exists, false if no such text exists anymore
+ private bool RemoveText0(int idIndex, uint textId)
+ {
+
+ m_textDB.RevertText(m_selectedLanguageFormat, textId);
+
+ string text = m_textDB.FindText(m_selectedLanguageFormat, textId);
+
+ if (text != null)
+ {
+ string entry = textId.ToString("X8") + " - " + text;
+ m_stringIdListBox.Items[idIndex] = entry;
+
+ return true;
+ }
+
+ return false;
+ }
+
+ ///
+ /// Calls the appropriate methods to revert or delete an adjective entry. Returns whether or not the declinated adjective of the given id still exists afterwards.
+ ///
+ ///
+ ///
+ /// true if an adjective entry of the id still exists, false if no such entry exists anymore
+ private bool RevertAdjective0(int idIndex, uint adjectiveId)
+ {
+
+ m_textDB.RevertDeclinatedAdjective(m_selectedLanguageFormat, m_selectedResource, adjectiveId);
+
+ List declinations = m_textDB.GetDeclinatedAdjectives(m_selectedLanguageFormat, m_selectedResource, adjectiveId);
+
+ if (declinations != null && declinations.Count > 0)
+ {
+ string entry = adjectiveId.ToString("X8") + " - " + declinations[0];
+ m_stringIdListBox.Items[idIndex] = entry;
+
+ return true;
+ }
+
+ return false;
+ }
+
+ private void ShowReplaceWindow(object sender, RoutedEventArgs e)
+ {
+
+ List stringsWithId = new List();
+ foreach (string entry in m_stringIdListBox.Items)
+ {
+ stringsWithId.Add(entry);
+ }
+
+ ReplaceWindow replaceWindow = new ReplaceWindow(m_textDB, m_selectedLanguageFormat, stringsWithId, m_stringIdListBox.SelectedIndex)
+ {
+ Owner = Application.Current.MainWindow
+ };
+
+ bool? save = replaceWindow.ShowDialog();
+ if (save.HasValue && save.Value)
+ {
+ // the replace window currently updates the selection of this view.
+ uint lastReplacedId = m_textIdsList[replaceWindow.LastEditedIndex];
+
+ ReLoadStrings(sender, e);
+
+ SearchTextId(lastReplacedId);
+ }
+ }
+
+ private void Export(object sender, RoutedEventArgs e)
+ {
+ XmlExporter.Export(m_textDB, m_selectedLanguageFormat);
+ }
+
+ private void Import(object sender, RoutedEventArgs e)
+ {
+ XmlImporter.Import(m_textDB);
+
+ ReLoadStrings(sender, e);
+ }
+
+ #endregion operation buttons
+
+ #region -- Closing Handler --
+
+ public override void Closed()
+ {
+ m_closingHandler.OnEditorClose();
+ }
+
+ internal class ClosingHandler
+ {
+
+ private readonly List nonModalChildren = new List();
+
+ public void AddChildWindow(Window nonModalWindow)
+ {
+ nonModalChildren.Add(nonModalWindow);
+ nonModalWindow.Closed += OnChildClose;
+ }
+
+ public void OnChildClose(object sender, EventArgs e)
+ {
+ nonModalChildren.Remove((Window)sender);
+ }
+
+ public void OnEditorClose()
+ {
+ foreach (Window childWindow in nonModalChildren)
+ {
+ childWindow.Closed -= OnChildClose;
+ childWindow.Close();
+ }
+ }
+ }
+ #endregion
+
+ }
+}
diff --git a/Plugins/BiowareLocalizationPlugin/Controls/EditAdjectivesWindow.xaml b/Plugins/BiowareLocalizationPlugin/Controls/EditAdjectivesWindow.xaml
new file mode 100644
index 000000000..9d3032300
--- /dev/null
+++ b/Plugins/BiowareLocalizationPlugin/Controls/EditAdjectivesWindow.xaml
@@ -0,0 +1,77 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Plugins/BiowareLocalizationPlugin/Controls/EditAdjectivesWindow.xaml.cs b/Plugins/BiowareLocalizationPlugin/Controls/EditAdjectivesWindow.xaml.cs
new file mode 100644
index 000000000..230503edd
--- /dev/null
+++ b/Plugins/BiowareLocalizationPlugin/Controls/EditAdjectivesWindow.xaml.cs
@@ -0,0 +1,219 @@
+using Frosty.Controls;
+using Frosty.Core;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+
+namespace BiowareLocalizationPlugin.Controls
+{
+
+ ///
+ /// Via this window the user can add or edit existing texts.
+ /// This is a positive logic only window, i.e., you can add texts to additional resource, but cannot deselect it again.
+ ///
+ [TemplatePart(Name = "adjectiveIdField", Type = typeof(TextBox))]
+ [TemplatePart(Name = "localizedAdjectiveListBox", Type = typeof(ListBox))]
+ [TemplatePart(Name = "resourcesListBox", Type = typeof(ListBox))]
+ [TemplatePart(Name = "saveButton", Type = typeof(Button))]
+ [TemplatePart(Name = "cancelButton", Type = typeof(Button))]
+ public partial class EditAdjectivesWindow : FrostyDockableWindow
+ {
+
+ ///
+ /// The save value tuple consiting of the adjective id, and the declinations or the adjective. This is only available after the save action!
+ ///
+ public Tuple> SaveValue { get; private set; }
+
+ private readonly string m_selectedLanguageFormat;
+ private readonly BiowareLocalizedStringDatabase m_stringDb;
+
+ ///
+ /// The resource(s?) where to edit the adjective
+ ///
+ private string m_selectedResource;
+
+ public EditAdjectivesWindow(BiowareLocalizedStringDatabase inStringDb, string inLanguageFormat, string inResource)
+ {
+ InitializeComponent();
+
+ m_selectedLanguageFormat = inLanguageFormat;
+ m_stringDb = inStringDb;
+
+ m_selectedResource = inResource;
+ }
+
+ ///
+ /// Initializes the window with the currently selected textid and text.
+ ///
+ ///
+ public void Init(uint adjectiveId)
+ {
+
+ if (adjectiveId != 0)
+ {
+ adjectiveIdField.Text = adjectiveId.ToString("X8");
+ }
+ resourcesListBox.Items.Add(m_selectedResource);
+ UpdateData(adjectiveId);
+ }
+
+ private void Update(object sender, RoutedEventArgs e)
+ {
+ uint adjectiveId = ReadAdjectiveId();
+ UpdateData(adjectiveId);
+ }
+
+ ///
+ /// Updates the listed data
+ ///
+ private void UpdateData(uint adjectiveId)
+ {
+
+ // revert;
+ localizedAdjectiveListBox.Items.Clear();
+
+ if (adjectiveId == 0 || m_selectedResource == null)
+ {
+ return;
+ }
+
+ List declinations = m_stringDb.GetDeclinatedAdjectives(m_selectedLanguageFormat, m_selectedResource, adjectiveId);
+
+ // workaround to get the required number of display entries:
+ if(declinations.Count == 0)
+ {
+ for(int i = 0; i< declinations.Capacity; i++)
+ {
+ declinations.Add(null);
+ }
+ }
+
+ foreach (string declination in declinations)
+ {
+ TextBox declinationBox = new TextBox
+ {
+ Text = declination,
+ IsReadOnly = false,
+ AcceptsReturn = false,
+ HorizontalAlignment = HorizontalAlignment.Stretch,
+ MinWidth = 60
+ };
+
+ localizedAdjectiveListBox.Items.Add(declinationBox);
+ }
+ }
+
+ private void Save(object sender, RoutedEventArgs e)
+ {
+ uint adjectiveId = ReadAdjectiveId();
+
+ if (adjectiveId != 0 && m_selectedResource != null)
+ {
+
+ List adjectiveDeclinations = new List();
+ foreach(TextBox declinationBox in localizedAdjectiveListBox.Items)
+ {
+ adjectiveDeclinations.Add(declinationBox.Text);
+ }
+ m_stringDb.SetDeclinatedAdjectve(m_selectedLanguageFormat, m_selectedResource, adjectiveId, adjectiveDeclinations);
+
+ SaveValue = Tuple.Create(adjectiveId, adjectiveDeclinations);
+
+
+ DialogResult = true;
+ Close();
+ }
+ }
+
+ ///
+ /// Closes the dialog without saving
+ ///
+ ///
+ ///
+ private void Close(object sender, RoutedEventArgs e)
+ {
+ DialogResult = false;
+ SaveValue = null;
+ Close();
+ }
+
+ ///
+ /// Reads the set hexadecimal text id and sets the lokal variable with the parsed result.
+ ///
+ /// _textId
+ private uint ReadAdjectiveId()
+ {
+ string text = adjectiveIdField.Text;
+ bool canRead = uint.TryParse(text, NumberStyles.HexNumber, null, out uint textId);
+ if (canRead)
+ {
+ return textId;
+ }
+
+ adjectiveIdField.Text = "";
+ App.Logger.LogWarning("Bad Input!");
+ return 0;
+ }
+
+ ///
+ /// Shows a popup dialog to select one or more resources where to add the current text into.
+ ///
+ ///
+ ///
+ private void AddResources(object sender, RoutedEventArgs e)
+ {
+
+ IEnumerable selectableResources = m_stringDb.GetAllResourceNames(m_selectedLanguageFormat)
+ .Where(r => !resourcesListBox.Items.Contains(r));
+
+ ResourceSelectionWindow selectionDialog = new ResourceSelectionWindow(selectableResources, SelectionMode.Single);
+ bool? saved = selectionDialog.ShowDialog();
+
+ if (saved != null && saved.Value)
+ {
+
+ int selectionCount = selectionDialog.SelectedResources.Count;
+
+ if (selectionCount == 0)
+ {
+ return;
+ }
+
+ // MultiSelect is actually not allowed here!
+ if( selectionDialog.SelectedResources.Count > 1 )
+ {
+ App.Logger.LogError("Can only select a single resource for adjectives at the moment!");
+ }
+
+ string resourceName = selectionDialog.SelectedResources[0];
+ resourcesListBox.Items.Add(resourceName);
+ m_selectedResource = resourceName;
+ }
+ }
+
+ private void RemoveResources(object sender, RoutedEventArgs e)
+ {
+
+ List selectedToRemove = resourcesListBox.SelectedItems.OfType().ToList();
+ foreach (string itemToRemove in selectedToRemove)
+ {
+ resourcesListBox.Items.Remove(itemToRemove);
+ }
+ m_selectedResource = null;
+ }
+
+ ///
+ /// Called to copy the selected values of either listbox to the clipboard.
+ ///
+ ///
+ ///
+ private void CopySelectionToClipboard(object sender, ExecutedRoutedEventArgs e)
+ {
+ ListBoxUtils.CopySelectionToClipboard(sender, e);
+ }
+ }
+}
diff --git a/Plugins/BiowareLocalizationPlugin/Controls/ImportTargetDialog.xaml b/Plugins/BiowareLocalizationPlugin/Controls/ImportTargetDialog.xaml
new file mode 100644
index 000000000..ccbc67e09
--- /dev/null
+++ b/Plugins/BiowareLocalizationPlugin/Controls/ImportTargetDialog.xaml
@@ -0,0 +1,109 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Plugins/BiowareLocalizationPlugin/Controls/ImportTargetDialog.xaml.cs b/Plugins/BiowareLocalizationPlugin/Controls/ImportTargetDialog.xaml.cs
new file mode 100644
index 000000000..0f073ca93
--- /dev/null
+++ b/Plugins/BiowareLocalizationPlugin/Controls/ImportTargetDialog.xaml.cs
@@ -0,0 +1,318 @@
+using BiowareLocalizationPlugin.ExportImport;
+using Frosty.Controls;
+using FrostySdk;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.Linq;
+using System.Text.RegularExpressions;
+using System.Windows;
+
+namespace BiowareLocalizationPlugin.Controls
+{
+ ///
+ /// Interaktionslogik für ImportTargetDialog.xaml
+ ///
+ public partial class ImportTargetDialog : FrostyDockableWindow
+ {
+
+ public TextFile SaveValue { get; set; }
+
+ public ObservableCollection GridSource = new ObservableCollection();
+
+ public List TargetResourceList { get; } = new List();
+
+ ///
+ /// Both ME:A and DA:I have this with the language as part of their (or most) text resources.
+ ///
+ private static readonly string m_TEXTTABLE_PATTERN = "/texttable/[a-z]+/";
+ private static readonly string m_TEXTTABLE_EN_PATH = "/texttable/en/";
+
+ // These resources change name depending on whether english or any other localization is used.
+ private static readonly string m_GLOBALMASTER = "globalmaster";
+ private static readonly string m_GLOBALTRANSLATED = "globaltranslated";
+
+ private readonly BiowareLocalizedStringDatabase m_textDb;
+ private readonly TextFile m_importTextFile;
+
+ private string m_selectedImportLanguageFormat;
+
+ public ImportTargetDialog(BiowareLocalizedStringDatabase inTextDB, TextFile inTextFile)
+ {
+ InitializeComponent();
+ Owner = Application.Current.MainWindow;
+
+ m_textDb = inTextDB;
+ m_importTextFile = inTextFile;
+
+ // TODO disable import button while not all texts are set!
+
+ InitLanguage(m_importTextFile.LanguageFormat);
+ InitResources();
+ }
+
+ public void LanguageFormatChanged(object sender, RoutedEventArgs e)
+ {
+ m_selectedImportLanguageFormat = (string)languageSelector.SelectedItem;
+
+ FillTargetResourceList();
+
+ string targetTextTablePath = GetTargetTexttableSubstring();
+ foreach (ResourceRow entry in GridSource)
+ {
+ entry.TargetResource = GetTargetResourceFor(entry.TextResource, targetTextTablePath);
+ }
+ }
+
+ public void Import(object sender, RoutedEventArgs e)
+ {
+
+ Dictionary resourceTranslation = new Dictionary();
+ foreach (ResourceRow resourceRow in GridSource)
+ {
+
+ if (resourceRow.TargetResource == null)
+ {
+
+ string msg = string.Format("No target resource for <{0}> selected!", resourceRow.TextResource);
+ MessageBox.Show(msg, "Missing Entry", MessageBoxButton.OK);
+
+ return;
+ }
+
+ resourceTranslation.Add(resourceRow.TextResource, resourceRow.TargetResource);
+ }
+
+ TextFile updatedTarget = new TextFile()
+ {
+ LanguageFormat = m_selectedImportLanguageFormat
+ };
+
+
+ TextRepresentation[] targetTextRepresentations = CreateTargetTextRepresentations(resourceTranslation);
+ updatedTarget.Texts = targetTextRepresentations;
+
+ DeclinatedAdjectiveRepresentation[] targetAdjectiveRepresentations = CreateTargetAdjectiveRepresentations(resourceTranslation);
+ updatedTarget.DeclinatedAdjectives = targetAdjectiveRepresentations;
+
+ SaveValue = updatedTarget;
+ DialogResult = true;
+
+ Close();
+ }
+
+ public void Abort(object sender, RoutedEventArgs e)
+ {
+ SaveValue = null;
+ DialogResult = false;
+ Close();
+ }
+
+ private void InitLanguage(string importLanguage)
+ {
+ languageTextBox.Text = importLanguage;
+
+ var availableLanguages = m_textDb.GellAllLanguages();
+ languageSelector.ItemsSource = availableLanguages;
+
+ languageSelector.SelectionChanged += LanguageFormatChanged;
+
+ // index of uses compare, contains equality
+ int languageIndex = availableLanguages.ToList().IndexOf(importLanguage);
+ if (languageIndex >= 0)
+ {
+ languageSelector.SelectedIndex = languageIndex;
+ }
+ }
+
+ private void InitResources()
+ {
+ List importResourcesList = GetTextResources(m_importTextFile);
+
+ FillTargetResourceList();
+
+ string targetTextTablePath = GetTargetTexttableSubstring();
+
+ GridSource.Clear();
+ foreach (string importResourceName in importResourcesList)
+ {
+
+ GridSource.Add(new ResourceRow()
+ {
+ TextResource = importResourceName,
+ TargetResource = GetTargetResourceFor(importResourceName, targetTextTablePath)
+ });
+ }
+
+ datagrid.ItemsSource = GridSource;
+ targetResources.ItemsSource = TargetResourceList;
+ }
+
+ private static List GetTextResources(TextFile textFile)
+ {
+ HashSet importResourcesSet = new HashSet();
+ foreach (var text in textFile.Texts)
+ {
+ importResourcesSet.UnionWith(text.Resources);
+ }
+
+ if (textFile.DeclinatedAdjectives != null)
+ {
+ foreach (var adjectiveEntry in textFile.DeclinatedAdjectives)
+ {
+ importResourcesSet.Add(adjectiveEntry.Resource);
+ }
+ }
+
+ List importResourcesList = importResourcesSet.ToList();
+ importResourcesList.Sort();
+ return importResourcesList;
+ }
+
+ private void FillTargetResourceList()
+ {
+ TargetResourceList.Clear();
+
+ if (m_selectedImportLanguageFormat != null)
+ {
+ TargetResourceList.AddRange(GetTargetResourceList(m_selectedImportLanguageFormat));
+ }
+ }
+
+ private List GetTargetResourceList(string languageFormat)
+ {
+ return m_textDb.GetAllResourceNames(languageFormat).ToList();
+ }
+
+ private string GetTargetResourceFor(string importResource, string targetTextTablePath)
+ {
+ if (TargetResourceList.Count > 0)
+ {
+ // indexOf uses equals instead of identity compare - which should result in less false negatives
+ int index = TargetResourceList.IndexOf(importResource);
+ if (index >= 0)
+ {
+ return TargetResourceList[index];
+ }
+
+ // MEA and DAI have the localization files (or most of them) within paths such as /dlc/../texttable/{language_short}/...
+ // -> Try to replace substring '/texttable/{language_short}/' with whatever is appropriate...
+ string importTextTablePath = GetTextTableSubString(importResource);
+ if (importTextTablePath != null && targetTextTablePath != null)
+ {
+ return FindWithReplacedPathInfo(importResource, importTextTablePath, targetTextTablePath);
+ }
+ }
+
+ return null;
+ }
+
+ private string GetTargetTexttableSubstring()
+ {
+ if (TargetResourceList.Count > 0)
+ {
+ var firstEntry = TargetResourceList[0];
+ return GetTextTableSubString(firstEntry);
+ }
+ return null;
+ }
+
+ private static string GetTextTableSubString(string textDonor)
+ {
+ Match match = Regex.Match(textDonor, m_TEXTTABLE_PATTERN);
+ return match.Success ? match.Value : null;
+ }
+
+ private string FindWithReplacedPathInfo(string importResource, string importTextTablePath, string targetTextTablePath)
+ {
+ string targetResource = importResource.Replace(importTextTablePath, targetTextTablePath);
+
+ // globalmaster resources in english are named globaltranslated for other languages
+ // for ME:A the globaltranslated resources are also in a subfolder, whereas globalmaster is not!
+ if (importResource.Contains(m_GLOBALMASTER) && !targetTextTablePath.Equals(m_TEXTTABLE_EN_PATH))
+ {
+ targetResource = targetResource.Replace(m_GLOBALMASTER, m_GLOBALTRANSLATED);
+
+ if (ProfilesLibrary.DataVersion == ((int)ProfileVersion.MassEffectAndromeda))
+ {
+ targetResource = targetResource.Replace("/game/globaltranslated", "/game/localization/config/globaltranslated");
+ }
+ }
+ else if (importResource.Contains(m_GLOBALTRANSLATED) && targetTextTablePath.Equals(m_TEXTTABLE_EN_PATH))
+ {
+ targetResource = targetResource.Replace(m_GLOBALTRANSLATED, m_GLOBALMASTER);
+
+ if (ProfilesLibrary.DataVersion == ((int)ProfileVersion.MassEffectAndromeda))
+ {
+ targetResource = targetResource.Replace("/game/localization/config/globalmaster", "/game/globalmaster");
+ }
+ }
+
+ int index = TargetResourceList.IndexOf(targetResource);
+
+ return index >= 0 ? TargetResourceList[index] : null;
+ }
+
+ private TextRepresentation[] CreateTargetTextRepresentations(Dictionary resourceTranslation)
+ {
+ List targetRepresentations = new List();
+ foreach (TextRepresentation importRepresentation in m_importTextFile.Texts)
+ {
+ TextRepresentation updatedRepresentation = new TextRepresentation()
+ {
+ TextId = importRepresentation.TextId,
+ Text = importRepresentation.Text,
+ Resources = importRepresentation.Resources.Select(r => resourceTranslation[r]).ToArray()
+ };
+
+ targetRepresentations.Add(updatedRepresentation);
+ }
+ return targetRepresentations.ToArray();
+ }
+
+ private DeclinatedAdjectiveRepresentation[] CreateTargetAdjectiveRepresentations(Dictionary resourceTranslation)
+ {
+
+ if (m_importTextFile.DeclinatedAdjectives == null)
+ {
+ return null;
+ }
+
+ List targetRepresentations = new List();
+ foreach (DeclinatedAdjectiveRepresentation importRepresentation in m_importTextFile.DeclinatedAdjectives)
+ {
+ DeclinatedAdjectiveRepresentation updatedRepresentation = new DeclinatedAdjectiveRepresentation()
+ {
+ Resource = resourceTranslation[importRepresentation.Resource],
+ AdjectiveId = importRepresentation.AdjectiveId,
+ Declinations = importRepresentation.Declinations
+ };
+
+ targetRepresentations.Add(updatedRepresentation);
+ }
+ return targetRepresentations.ToArray();
+ }
+
+ }
+
+ #region -- ui data field stuff --
+ public class ResourceRow : INotifyPropertyChanged
+ {
+ public string TextResource { get; set; }
+
+ private string _targetResource;
+ public string TargetResource
+ {
+ get { return _targetResource; }
+
+ set
+ {
+ _targetResource = value;
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(TargetResource)));
+ }
+ }
+
+ public event PropertyChangedEventHandler PropertyChanged;
+ }
+ #endregion
+}
diff --git a/Plugins/BiowareLocalizationPlugin/Controls/ListBoxUtils.cs b/Plugins/BiowareLocalizationPlugin/Controls/ListBoxUtils.cs
new file mode 100644
index 000000000..d3aa53c1f
--- /dev/null
+++ b/Plugins/BiowareLocalizationPlugin/Controls/ListBoxUtils.cs
@@ -0,0 +1,56 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Text;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+
+namespace BiowareLocalizationPlugin.Controls
+{
+ ///
+ /// Class with some utility methods for handling io out of listboxes
+ ///
+ public class ListBoxUtils
+ {
+
+ private ListBoxUtils()
+ {
+ // prevent instantiation
+ }
+
+ public static void SortListIntoListBox(IEnumerable entries, ListBox listBox) where T : IComparable
+ {
+ var entryList = new List(entries);
+ entryList.Sort();
+
+ foreach(T entry in entryList)
+ {
+ listBox.Items.Add(entry);
+ }
+ }
+
+ ///
+ /// Called to copy the selected values of either listbox to the clipboard.
+ ///
+ /// The originator of the event
+ /// The event
+ public static void CopySelectionToClipboard(object listBoxEventOriginator, ExecutedRoutedEventArgs eventArgs)
+ {
+ ListBox origin = (ListBox)listBoxEventOriginator;
+
+ IList selection = origin.SelectedItems;
+
+ if (selection.Count > 0)
+ {
+ StringBuilder sb = new StringBuilder();
+ foreach (object selected in selection)
+ {
+ sb.AppendLine(selected.ToString());
+ }
+
+ Clipboard.SetText(sb.ToString());
+ }
+ }
+ }
+}
diff --git a/Plugins/BiowareLocalizationPlugin/Controls/ReplaceWindow.xaml b/Plugins/BiowareLocalizationPlugin/Controls/ReplaceWindow.xaml
new file mode 100644
index 000000000..0f34caf04
--- /dev/null
+++ b/Plugins/BiowareLocalizationPlugin/Controls/ReplaceWindow.xaml
@@ -0,0 +1,154 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Plugins/BiowareLocalizationPlugin/Controls/ReplaceWindow.xaml.cs b/Plugins/BiowareLocalizationPlugin/Controls/ReplaceWindow.xaml.cs
new file mode 100644
index 000000000..132ec8e45
--- /dev/null
+++ b/Plugins/BiowareLocalizationPlugin/Controls/ReplaceWindow.xaml.cs
@@ -0,0 +1,332 @@
+using Frosty.Controls;
+using Frosty.Core;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Documents;
+using System.Windows.Media;
+
+namespace BiowareLocalizationPlugin.Controls
+{
+
+ ///
+ /// This window is used for replacing text parts within the currently shown list of strings.
+ ///
+ public partial class ReplaceWindow : FrostyDockableWindow
+ {
+ ///
+ /// The index of the last edited entry.
+ ///
+ public int LastEditedIndex { get; private set; }
+
+ ///
+ /// This dialogs list of strings, prepended by their id value.
+ /// I could have use a sorted dictionary like civilized man, but that would require refactoring the old main ui, which i don't currently want to.
+ ///
+ private readonly List m_textsWithIdList;
+
+ ///
+ /// The language selected in the main view.
+ ///
+ private readonly string m_selectedLanguageFormat;
+
+ ///
+ /// The actual database object.
+ ///
+ private readonly BiowareLocalizedStringDatabase m_stringDb;
+
+ ///
+ /// Marker whether any text was replaced at all.
+ ///
+ private bool m_wasAnythingReplaced = false;
+
+ ///
+ /// The currently selected index.
+ ///
+ private int m_selectedIndex;
+
+ public ReplaceWindow(BiowareLocalizedStringDatabase inStringDb, string inSelectedLanguage, List inTextWithIds, int inSelectedIndex = -1)
+ {
+ InitializeComponent();
+
+ m_stringDb = inStringDb;
+ m_selectedLanguageFormat = inSelectedLanguage;
+ m_textsWithIdList = inTextWithIds;
+ m_selectedIndex = inSelectedIndex;
+ LastEditedIndex = 0;
+ }
+
+ // https://stackoverflow.com/questions/53319918/highlighting-coloring-charactars-in-a-wpf-richtextbox
+ private static void HighlightText(RichTextBox richTextBox, int startPoint, int endPoint, Color color)
+ {
+ //Trying to highlight charactars here
+ TextPointer pointer = richTextBox.Document.ContentStart;
+ TextRange range = new TextRange(pointer.GetPositionAtOffset(startPoint), pointer.GetPositionAtOffset(endPoint));
+ range.ApplyPropertyValue(TextElement.BackgroundProperty, new SolidColorBrush(color));
+ }
+
+ ///
+ /// Sest the given text into the given textbox and highlights the given consecutive (!) charachter offsets in the given color.
+ ///
+ ///
+ ///
+ ///
+ ///
+ private static void SetTextAndHighlightParts(RichTextBox textBox, string textToSet, List partsToHighlight, Color color)
+ {
+
+ Paragraph paragraph = new Paragraph(new Run(textToSet))
+ {
+ TextIndent = 0d,
+ };
+ textBox.Document.Blocks.Add(paragraph);
+
+ int charOffset = 2;
+ // the highlighting method works with texpointers which include non character symbols, like start and end tags for the hightlighting...
+ foreach (int[] segment in partsToHighlight)
+ {
+ HighlightText(textBox, segment[0] + charOffset, segment[1] + charOffset, color);
+ charOffset += 4;
+ }
+ }
+
+ ///
+ /// Finds the next text in the textSelectionBox which contains the searched for string.
+ ///
+ ///
+ ///
+ private void FindNext(object sender, RoutedEventArgs e)
+ {
+ Find(i => i + 1);
+ }
+
+ ///
+ /// Finds the previous text in the textSelectionBox which contains the searched for string.
+ ///
+ ///
+ ///
+ private void FindPrevious(object sender, RoutedEventArgs e)
+ {
+ Find(i => i - 1);
+ }
+
+ private void Find(Func indexUpdateFunction)
+ {
+ ClearSelection();
+
+ string searchedFor = Part_ToFindTextField.Text;
+ if (searchedFor == null || searchedFor.Length == 0)
+ {
+ return;
+ }
+
+ m_selectedIndex = indexUpdateFunction(m_selectedIndex);
+ int maxTextEntries = m_textsWithIdList.Count;
+ if (m_selectedIndex < 0 )
+ {
+ m_selectedIndex = -1;
+ return;
+ }
+ else if(m_selectedIndex >= maxTextEntries)
+ {
+ m_selectedIndex = maxTextEntries;
+ return;
+ }
+
+ bool? nullableSearchCaseSensitive = Part_CaseSensitiveSearchCB.IsChecked;
+ bool searchCaseSensitive = nullableSearchCaseSensitive.HasValue && nullableSearchCaseSensitive.Value;
+ StringComparison comparisonType = searchCaseSensitive ? StringComparison.InvariantCulture : StringComparison.InvariantCultureIgnoreCase;
+
+ while (m_selectedIndex >= 0 && m_selectedIndex < maxTextEntries)
+ {
+ string queriedText = m_textsWithIdList[m_selectedIndex];
+
+ // first 8 chars are the text Id, followed by 3 chars delimiter -> text starts at index 11
+ int searchTextPosition = queriedText.IndexOf(searchedFor, 11, comparisonType);
+ if (searchTextPosition > 0)
+ {
+ HandleFoundTextToReplace(queriedText, searchedFor, searchCaseSensitive);
+ return;
+ }
+
+ m_selectedIndex = indexUpdateFunction(m_selectedIndex);
+ }
+ }
+
+ ///
+ /// Clears the current selection, in order to prepare for the next finding that may never come.
+ ///
+ private void ClearSelection()
+ {
+
+
+ Part_TextIdField.Text = "";
+
+ Part_OriginalTextBox.Document.Blocks.Clear();
+ Part_EditedTextBox.Document.Blocks.Clear();
+ }
+
+ ///
+ /// Creates the replacement text and updates all ui elements with the new data.
+ ///
+ ///
+ ///
+ ///
+ private void HandleFoundTextToReplace(string selectedTextEntryWithId, string searchedText, bool searchCaseSensitive)
+ {
+ Part_TextIdField.Text = selectedTextEntryWithId.Substring(0, 8);
+
+ string originalTextToDisplay = selectedTextEntryWithId.Substring(11);
+
+ List partsToReplace = GetSearchPositionsInOriginalText(originalTextToDisplay, searchedText, searchCaseSensitive);
+ SetTextAndHighlightParts(Part_OriginalTextBox, originalTextToDisplay, partsToReplace, Colors.DarkRed);
+
+ string replaceWith = Part_ReplaceWithField.Text;
+
+ string replacedText = GetEditTextAndUpdatePositions(originalTextToDisplay, replaceWith, partsToReplace);
+ SetTextAndHighlightParts(Part_EditedTextBox, replacedText, partsToReplace, Colors.DarkGreen);
+ }
+
+ ///
+ /// Returns a list of character posisitons (as array of start and end position) of each finding in the given original text.
+ ///
+ ///
+ ///
+ ///
+ ///
+ private List GetSearchPositionsInOriginalText(string originalText, string toReplace, bool searchCaseSensitive)
+ {
+
+ StringComparison comparetype = searchCaseSensitive ? StringComparison.InvariantCulture : StringComparison.InvariantCultureIgnoreCase;
+ int toReplaceLength = toReplace.Length;
+
+ int currentPartStart = 0;
+ int lastPartEnd = 0;
+
+ List positionsToReplace = new List();
+
+ while (currentPartStart >= 0)
+ {
+ currentPartStart = originalText.IndexOf(toReplace, lastPartEnd, comparetype);
+
+ if (currentPartStart >= 0)
+ {
+ lastPartEnd = currentPartStart + toReplaceLength;
+
+ positionsToReplace.Add(new int[] { currentPartStart, lastPartEnd });
+ }
+ }
+
+ return positionsToReplace;
+ }
+
+ ///
+ /// Returns a new string, replacing each of the given positionsToReplace in the given originalText with the replacement.
+ /// Also updates the list of positions, so they now show the new offset(s) for the replacement string.
+ ///
+ ///
+ ///
+ ///
+ ///
+ private string GetEditTextAndUpdatePositions(string originalText, string replacement, List positionsToReplace)
+ {
+ int replacementLength = replacement.Length;
+
+ int lastSelectPosition = 0;
+ int currentWritePosition = 0;
+
+ StringBuilder sb = new StringBuilder();
+
+ foreach (int[] segment in positionsToReplace)
+ {
+
+ // part before replace
+ int selectLength = segment[0] - lastSelectPosition;
+ string partBefore = originalText.Substring(lastSelectPosition, selectLength);
+
+ sb.Append(partBefore);
+ currentWritePosition += selectLength;
+ segment[0] = currentWritePosition;
+
+ // replace
+ sb.Append(replacement);
+
+ lastSelectPosition = segment[1];
+ currentWritePosition += replacementLength;
+ segment[1] = currentWritePosition;
+ }
+
+ sb.Append(originalText.Substring(lastSelectPosition));
+
+ return sb.ToString();
+ }
+
+ ///
+ /// Basically 'save': Sets the current edit textbox's content as the new text for the text id in all resources that it currently exists in.
+ ///
+ ///
+ ///
+ private void Replace(object sender, RoutedEventArgs e)
+ {
+ string stringIdAsText = Part_TextIdField.Text;
+
+ if (stringIdAsText == null || stringIdAsText.Length == 0)
+ {
+ App.Logger.LogError("No Text Id set!");
+ return;
+ }
+
+ bool canReadId = uint.TryParse(stringIdAsText, NumberStyles.HexNumber, null, out uint textId);
+ if (!canReadId)
+ {
+ App.Logger.LogError("Cannot read text id: <{0}>", stringIdAsText);
+ return;
+ }
+
+ string editedText = GetEditedReplacementText();
+
+ var resourceNames = m_stringDb.GetAllLocalizedStringResourcesForTextId(m_selectedLanguageFormat, textId).Select(r => r.Name).ToList();
+ m_stringDb.SetText(m_selectedLanguageFormat, resourceNames, textId, editedText);
+
+ App.Logger.Log("Replaced text <{0}>", stringIdAsText);
+
+ m_wasAnythingReplaced = true;
+
+ // also replace the text in memory of this dialog
+ string replacedListText = stringIdAsText + " - " + editedText;
+ m_textsWithIdList[m_selectedIndex] = replacedListText;
+
+ LastEditedIndex = m_selectedIndex;
+ }
+
+ // https://learn.microsoft.com/en-us/dotnet/desktop/wpf/controls/how-to-extract-the-text-content-from-a-richtextbox
+ // Why didn't they just add that as default method in the fuckin richtextbox in the first place?
+ private string GetEditedReplacementText()
+ {
+ TextRange textRange = new TextRange(
+ Part_EditedTextBox.Document.ContentStart,
+ Part_EditedTextBox.Document.ContentEnd
+ );
+
+ string text = textRange.Text.TrimEnd("\r\n".ToCharArray());
+ return text;
+ }
+
+ private void ReplaceAndFindNext(object sender, RoutedEventArgs e)
+ {
+ Replace(sender, e);
+ FindNext(sender, e);
+ }
+
+ private void Close(object sender, RoutedEventArgs e)
+ {
+ DialogResult = m_wasAnythingReplaced;
+
+ Close();
+ }
+ }
+}
diff --git a/Plugins/BiowareLocalizationPlugin/Controls/ResourceSelectionWindow.xaml b/Plugins/BiowareLocalizationPlugin/Controls/ResourceSelectionWindow.xaml
new file mode 100644
index 000000000..2bbac826b
--- /dev/null
+++ b/Plugins/BiowareLocalizationPlugin/Controls/ResourceSelectionWindow.xaml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Plugins/BiowareLocalizationPlugin/Controls/ResourceSelectionWindow.xaml.cs b/Plugins/BiowareLocalizationPlugin/Controls/ResourceSelectionWindow.xaml.cs
new file mode 100644
index 000000000..70c0ada49
--- /dev/null
+++ b/Plugins/BiowareLocalizationPlugin/Controls/ResourceSelectionWindow.xaml.cs
@@ -0,0 +1,53 @@
+using Frosty.Controls;
+using System.Collections.Generic;
+using System.Windows;
+using System.Windows.Controls;
+
+namespace BiowareLocalizationPlugin.Controls
+{
+ ///
+ /// Interaktionslogik für ResourceSelectionWindow.xaml
+ ///
+ [TemplatePart(Name = "resourcesListBox", Type =typeof(ListBox))]
+ public partial class ResourceSelectionWindow : FrostyDockableWindow
+ {
+
+ ///
+ /// The list of selected resources that is the return value of this dialog.
+ ///
+ public List SelectedResources { get; private set; }
+
+ ///
+ /// Creates a new resource selection dialog.
+ ///
+ /// The list of selectable resources to display.
+ public ResourceSelectionWindow(IEnumerable inResourcesToShow)
+ {
+ InitializeComponent();
+ Loaded += (s, e) => ListBoxUtils.SortListIntoListBox(inResourcesToShow, resourcesListBox);
+ }
+
+ public ResourceSelectionWindow(IEnumerable inResourcesToShow, SelectionMode inResourceSelectionMode) : this(inResourcesToShow)
+ {
+ Loaded += (s, e) => resourcesListBox.SelectionMode = inResourceSelectionMode;
+ }
+
+ private void Save(object sender, RoutedEventArgs e)
+ {
+
+ SelectedResources = new List();
+ foreach(string selected in resourcesListBox.SelectedItems)
+ {
+ SelectedResources.Add(selected);
+ }
+ DialogResult = true;
+ Close();
+ }
+
+ private void Cancel(object sender, RoutedEventArgs e)
+ {
+ DialogResult = false;
+ Close();
+ }
+ }
+}
diff --git a/Plugins/BiowareLocalizationPlugin/Controls/SearchFindWindow.xaml b/Plugins/BiowareLocalizationPlugin/Controls/SearchFindWindow.xaml
new file mode 100644
index 000000000..ff68d6882
--- /dev/null
+++ b/Plugins/BiowareLocalizationPlugin/Controls/SearchFindWindow.xaml
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Plugins/BiowareLocalizationPlugin/Controls/SearchFindWindow.xaml.cs b/Plugins/BiowareLocalizationPlugin/Controls/SearchFindWindow.xaml.cs
new file mode 100644
index 000000000..6c4d6b34d
--- /dev/null
+++ b/Plugins/BiowareLocalizationPlugin/Controls/SearchFindWindow.xaml.cs
@@ -0,0 +1,83 @@
+using Frosty.Controls;
+using System;
+using System.Windows;
+using System.Windows.Controls;
+
+namespace BiowareLocalizationPlugin.Controls
+{
+ ///
+ /// This is a non blocking dialog used to search for strings in the currently displayed texts.
+ ///
+ public partial class SearchFindWindow : FrostyDockableWindow
+ {
+
+ // this is the list box from the main edit window - we search and select directly on there!
+ private readonly ListBox m_mainWindowTextSelectionBox;
+
+ public SearchFindWindow(ListBox inStringSelectionBox)
+ {
+ InitializeComponent();
+ Owner = Application.Current.MainWindow;
+ m_mainWindowTextSelectionBox = inStringSelectionBox;
+
+ searchTextField.Focus();
+ }
+
+ private void Close(object sender, RoutedEventArgs e)
+ {
+ Close();
+ }
+
+ ///
+ /// Searches the text, if found the entry is selected and scrolled into view.
+ ///
+ ///
+ ///
+ private void Search(object sender, RoutedEventArgs e)
+ {
+
+ string searchedFor = searchTextField.Text;
+ if(searchedFor == null || searchedFor.Length == 0)
+ {
+ return;
+ }
+
+ bool? nullableSearchBackwards = backSearchCB.IsChecked;
+ bool searchBackwards = nullableSearchBackwards.HasValue && nullableSearchBackwards.Value;
+
+ bool? nullableSearchCaseSensitive = caseSensitiveSearchCB.IsChecked;
+ bool searchCaseSensitive = nullableSearchCaseSensitive.HasValue && nullableSearchCaseSensitive.Value;
+
+ Func updFctn;
+ if( searchBackwards)
+ {
+ updFctn = (int i) => --i;
+ }
+ else
+ {
+ updFctn = (int j) => ++j;
+ }
+
+ StringComparison comparisonType = searchCaseSensitive ? StringComparison.InvariantCulture : StringComparison.InvariantCultureIgnoreCase;
+
+ int searchIndex = m_mainWindowTextSelectionBox.SelectedIndex;
+ searchIndex = updFctn(searchIndex);
+
+ while(searchIndex >=0 && searchIndex < m_mainWindowTextSelectionBox.Items.Count)
+ {
+ string queriedText = (string)m_mainWindowTextSelectionBox.Items[searchIndex];
+
+ // first 8 chars are the text Id, followed by 3 chars delimiter -> text starts at index 11
+ int searchTextPosition = queriedText.IndexOf(searchedFor, 10, comparisonType);
+ if(searchTextPosition > 0)
+ {
+ m_mainWindowTextSelectionBox.SelectedIndex = searchIndex;
+ m_mainWindowTextSelectionBox.ScrollIntoView(m_mainWindowTextSelectionBox.SelectedItem);
+ return;
+ }
+
+ searchIndex = updFctn(searchIndex);
+ }
+ }
+ }
+}
diff --git a/Plugins/BiowareLocalizationPlugin/Controls/TextInfoWindow.xaml b/Plugins/BiowareLocalizationPlugin/Controls/TextInfoWindow.xaml
new file mode 100644
index 000000000..e0cb3b58e
--- /dev/null
+++ b/Plugins/BiowareLocalizationPlugin/Controls/TextInfoWindow.xaml
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Plugins/BiowareLocalizationPlugin/Controls/TextInfoWindow.xaml.cs b/Plugins/BiowareLocalizationPlugin/Controls/TextInfoWindow.xaml.cs
new file mode 100644
index 000000000..7dcb16115
--- /dev/null
+++ b/Plugins/BiowareLocalizationPlugin/Controls/TextInfoWindow.xaml.cs
@@ -0,0 +1,85 @@
+using BiowareLocalizationPlugin.LocalizedResources;
+using Frosty.Controls;
+using System.Collections.Generic;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+
+namespace BiowareLocalizationPlugin.Controls
+{
+ ///
+ /// This is a popup window that displays in which resources the text of the selected id is found, it also displays what other text ids are stored at the same position(s) in those resources.
+ ///
+ [TemplatePart(Name = "resourceList", Type = typeof(ListBox))]
+ [TemplatePart(Name = "stringIdList", Type = typeof(ListBox))]
+ [TemplatePart(Name = "charactersList", Type = typeof(ListBox))]
+ [TemplatePart(Name = "PART_OkButton", Type = typeof(Button))]
+ public partial class TextInfoWindow : FrostyDockableWindow
+ {
+
+ public TextInfoWindow()
+ {
+ InitializeComponent();
+ Owner = Application.Current.MainWindow;
+ }
+
+ public void Init(string languageFormat, uint textId, BiowareLocalizedStringDatabase localizedStringsDb)
+ {
+
+ Title = "TextInfo: " + textId.ToString("X8") + " - " + localizedStringsDb.GetString(textId);
+
+ IEnumerable localizedResources = localizedStringsDb.GetAllLocalizedStringResourcesForTextId(languageFormat, textId);
+
+ // multiple text ids can occur in multiple places
+ var allResourceNames = new List();
+ var allUniqueTextIds = new SortedSet();
+ SortedSet allSupportedCharacters = null;
+ foreach (LocalizedStringResource resource in localizedResources)
+ {
+ // add the resource name...
+ allResourceNames.Add(resource.Name);
+
+ // ...add the other texts ids that share the position and such text...
+ IEnumerable textIds = resource.GetAllTextIdsAtPositionOf(textId);
+ foreach (string anotherTextId in textIds)
+ {
+ allUniqueTextIds.Add(anotherTextId);
+ }
+
+ // ... add or rather retain the limited set of characters supported in all the of the resources
+ var supporedCharacters = resource.GetDefaultSupportedCharacters();
+ if (allSupportedCharacters == null)
+ {
+ allSupportedCharacters = new SortedSet(supporedCharacters);
+ }
+ else
+ {
+ allSupportedCharacters.IntersectWith(supporedCharacters);
+ }
+ }
+
+ ListBoxUtils.SortListIntoListBox(allResourceNames, resourceList);
+ ListBoxUtils.SortListIntoListBox(allUniqueTextIds, stringIdList);
+
+ if (allSupportedCharacters != null)
+ {
+ ListBoxUtils.SortListIntoListBox(allSupportedCharacters, charactersList);
+ }
+ }
+
+ private void OkButtonClicked(object sender, RoutedEventArgs e)
+ {
+ Close();
+ }
+
+ ///
+ /// Called to copy the selected values of either listbox to the clipboard.
+ ///
+ ///
+ ///
+ private void CopySelection(object sender, ExecutedRoutedEventArgs e)
+ {
+ ListBoxUtils.CopySelectionToClipboard(sender, e);
+ }
+ }
+}
diff --git a/Plugins/BiowareLocalizationPlugin/ExportImport/TextRepresentation.cs b/Plugins/BiowareLocalizationPlugin/ExportImport/TextRepresentation.cs
new file mode 100644
index 000000000..16b41dd5f
--- /dev/null
+++ b/Plugins/BiowareLocalizationPlugin/ExportImport/TextRepresentation.cs
@@ -0,0 +1,46 @@
+using System;
+using System.Xml.Serialization;
+
+namespace BiowareLocalizationPlugin.ExportImport
+{
+ [Serializable()]
+ [XmlRoot("TextFile", Namespace = "", IsNullable = false)]
+ public class TextFile
+ {
+
+ public string LanguageFormat { get; set; }
+
+ [XmlArray("Texts")]
+ [XmlArrayItem("TextRepresentation")]
+ public TextRepresentation[] Texts { get; set; }
+
+ //[CanBeNull]
+ [XmlArray("DeclinatedAdjectives", IsNullable = true)]
+ [XmlArrayItem("DeclinatedAdjective")]
+ public DeclinatedAdjectiveRepresentation[] DeclinatedAdjectives { get; set; }
+ }
+
+ [Serializable()]
+ public class TextRepresentation
+ {
+ public string TextId { get; set; }
+ public string Text { get; set; }
+
+ [XmlArray("Resources")]
+ [XmlArrayItem("TextResource")]
+ public string[] Resources { get; set; }
+ }
+
+ [Serializable()]
+ public class DeclinatedAdjectiveRepresentation
+ {
+
+ public string Resource { get; set; }
+
+ public string AdjectiveId { get; set; }
+
+ [XmlArray("Declinations")]
+ [XmlArrayItem("Declination")]
+ public string[] Declinations { get; set; }
+ }
+}
diff --git a/Plugins/BiowareLocalizationPlugin/ExportImport/XmlExporter.cs b/Plugins/BiowareLocalizationPlugin/ExportImport/XmlExporter.cs
new file mode 100644
index 000000000..4503d9da6
--- /dev/null
+++ b/Plugins/BiowareLocalizationPlugin/ExportImport/XmlExporter.cs
@@ -0,0 +1,184 @@
+
+using BiowareLocalizationPlugin.LocalizedResources;
+using Frosty.Controls;
+using Frosty.Core;
+using Frosty.Core.Controls;
+using Frosty.Core.Windows;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Windows;
+using System.Xml;
+using System.Xml.Serialization;
+
+namespace BiowareLocalizationPlugin.ExportImport
+{
+ public class XmlExporter
+ {
+ public static void Export(BiowareLocalizedStringDatabase textDB, string languageFormat)
+ {
+
+ bool exportAll = false;
+ if (Config.Get(BiowareLocalizationPluginOptions.ASK_XML_EXPORT_OPTIONS, false, ConfigScope.Global))
+ {
+ var result = FrostyMessageBox.Show("Export All Texts?\r\nSelecting 'no' will export modified texts only.", "Export Options", MessageBoxButton.YesNo);
+ exportAll = MessageBoxResult.Yes == result;
+ }
+
+ FrostySaveFileDialog saveDialog = new FrostySaveFileDialog("Save Custom Texts", "*.xml (XML File)|*.xml", "LocalizedTexts_", languageFormat+"_texts");
+ if (saveDialog.ShowDialog())
+ {
+ FrostyTaskWindow.Show("Exporting Custom Texts", "", (task) =>
+ {
+
+ TextFile textFile = FillTextFile(textDB, languageFormat, exportAll);
+
+ XmlSerializer serializer = new XmlSerializer(typeof(TextFile));
+
+ XmlWriterSettings settings = new XmlWriterSettings()
+ {
+ Encoding = Encoding.UTF8,
+ Indent = true
+ };
+
+ using (FileStream fileStream = new FileStream(saveDialog.FileName, FileMode.Create))
+ {
+ using (XmlWriter writer = XmlWriter.Create(fileStream, settings))
+ {
+ serializer.Serialize(writer, textFile, new XmlSerializerNamespaces(new[] { XmlQualifiedName.Empty }));
+ }
+ }
+ });
+
+ App.Logger.Log("Custom texts saved to {0}", saveDialog.FileName);
+ }
+ }
+
+ private static TextFile FillTextFile(BiowareLocalizedStringDatabase textDB, string languageFormat, bool exportAll)
+ {
+
+ LanguageTextsDB localizedTextDB = textDB.GetLocalizedTextDB(languageFormat);
+
+ TextFile textFile = new TextFile
+ {
+ LanguageFormat = languageFormat
+ };
+
+ TextRepresentation[] textsArray = GetAllTextsToExport(localizedTextDB, exportAll);
+ textFile.Texts = textsArray;
+
+ DeclinatedAdjectiveRepresentation[] declinatedAdjectives = GetAllDeclinatedAdjectivesToExport(localizedTextDB, exportAll);
+ textFile.DeclinatedAdjectives = declinatedAdjectives;
+
+ return textFile;
+ }
+
+ private static List GetAllTextIdsToExport(LanguageTextsDB localizedTextDB, bool exportAll)
+ {
+
+ List textIdList;
+ if (exportAll)
+ {
+ textIdList = localizedTextDB.GetAllTextIds().ToList();
+ }
+ else
+ {
+ textIdList = localizedTextDB.GetAllModifiedTextsIds().ToList();
+ }
+
+ textIdList.Sort();
+ return textIdList;
+ }
+
+ private static TextRepresentation[] GetAllTextsToExport(LanguageTextsDB localizedTextDB, bool exportAll)
+ {
+
+ List textIdList = GetAllTextIdsToExport(localizedTextDB, exportAll);
+ List textList = new List(textIdList.Count);
+
+
+ foreach (uint textId in textIdList)
+ {
+ TextRepresentation textRepresentation = new TextRepresentation()
+ {
+ TextId = textId.ToString("X8"),
+ Text = localizedTextDB.GetText(textId)
+ };
+
+ List resourceNames = new List();
+ foreach (var resource in localizedTextDB.GetAllResourcesForTextId(textId))
+ {
+ resourceNames.Add(resource.Name);
+ }
+ textRepresentation.Resources = resourceNames.ToArray();
+
+ textList.Add(textRepresentation);
+ }
+
+ return textList.ToArray();
+ }
+
+ private static DeclinatedAdjectiveRepresentation[] GetAllDeclinatedAdjectivesToExport(LanguageTextsDB localizedTextDB, bool exportAll)
+ {
+
+ List resourceNames;
+ if(exportAll)
+ {
+ resourceNames = localizedTextDB.GetAllResourceNamesWithDeclinatedAdjectives().ToList();
+ }
+ else
+ {
+ resourceNames = localizedTextDB.GetAllResourceNamesWithModifiedDeclinatedAdjectives().ToList();
+ }
+
+ if(resourceNames.Count == 0)
+ {
+ return null;
+ }
+
+ return GetAllDeclinatedAdjectivesToExport(localizedTextDB, resourceNames, exportAll);
+ }
+
+ private static DeclinatedAdjectiveRepresentation[] GetAllDeclinatedAdjectivesToExport(LanguageTextsDB localizedTextDB, IEnumerable resourceNames, bool exportAll)
+ {
+
+ List< DeclinatedAdjectiveRepresentation > adjectiveRepresentations = new List< DeclinatedAdjectiveRepresentation >();
+ foreach(string resourceName in resourceNames)
+ {
+
+ IEnumerable adjectiveIds;
+
+ if(exportAll)
+ {
+ adjectiveIds = localizedTextDB.GetAllDeclinatedAdjectiveIdsFromResource(resourceName);
+ }
+ else
+ {
+ adjectiveIds = localizedTextDB.GetModifiedDeclinatedAdjectiveIdsFromResource(resourceName);
+ }
+
+ foreach(uint adjectiveId in adjectiveIds)
+ {
+
+ adjectiveRepresentations.Add(CreateDeclinationEntry(localizedTextDB, resourceName, adjectiveId));
+ }
+ }
+
+ return adjectiveRepresentations.ToArray();
+ }
+
+ private static DeclinatedAdjectiveRepresentation CreateDeclinationEntry(LanguageTextsDB languageTextsDB, string resourceName, uint adjectiveId)
+ {
+ List declinations = languageTextsDB.GetDeclinatedAdjectives(resourceName, adjectiveId);
+
+ return new DeclinatedAdjectiveRepresentation()
+ {
+ Resource = resourceName,
+ AdjectiveId = adjectiveId.ToString("X8"),
+ Declinations = declinations.ToArray()
+ };
+ }
+
+ }
+}
diff --git a/Plugins/BiowareLocalizationPlugin/ExportImport/XmlImporter.cs b/Plugins/BiowareLocalizationPlugin/ExportImport/XmlImporter.cs
new file mode 100644
index 000000000..645496cbf
--- /dev/null
+++ b/Plugins/BiowareLocalizationPlugin/ExportImport/XmlImporter.cs
@@ -0,0 +1,191 @@
+
+
+using BiowareLocalizationPlugin.Controls;
+using BiowareLocalizationPlugin.LocalizedResources;
+using Frosty.Core;
+using Frosty.Core.Controls;
+using Frosty.Core.Windows;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Security;
+using System.Xml.Serialization;
+
+
+namespace BiowareLocalizationPlugin.ExportImport
+{
+ public class XmlImporter
+ {
+ public static void Import(BiowareLocalizedStringDatabase textDb)
+ {
+ FrostyOpenFileDialog openDialog = new FrostyOpenFileDialog("Import Custom Texts", "*.xml (XML File)|*.xml", "LocalizedTexts_");
+ if (openDialog.ShowDialog())
+ {
+
+ string fileName = openDialog.FileName;
+ TextFile textFile = ReadTextFile(fileName);
+
+ if (textFile != null)
+ {
+ App.Logger.Log("Importing localized texts form File <{0}>", fileName);
+ textFile = AdaptTextFile(textDb, textFile);
+ if (textFile != null)
+ {
+ ImportTexts(textDb, textFile);
+ }
+ }
+ }
+ }
+
+ ///
+ /// Step one: Try to parse the file - if there are problems they likely occur here, resulting in an exception that rolls back the whole process.
+ ///
+ ///
+ ///
+ private static TextFile ReadTextFile(string fileUri)
+ {
+
+ TextFile textFile = null;
+ FrostyTaskWindow.Show("Reading Custom Texts", "", (task) =>
+ {
+
+ XmlSerializer deserializer = new XmlSerializer(typeof(TextFile));
+ try
+ {
+ using (FileStream stream = new FileStream(fileUri, FileMode.Open, FileAccess.Read))
+ {
+ textFile = deserializer.Deserialize(stream) as TextFile;
+ }
+ }
+ catch (Exception e) when
+ (
+ e is IOException
+ || e is SecurityException
+ || e is UnauthorizedAccessException
+ || e is InvalidOperationException
+ )
+ {
+ textFile = null;
+ App.Logger.LogError("Could not read the given file <{0}> or deserialize it!", fileUri);
+ App.Logger.LogError("Exception was: <{0}> {1}", e.GetType(), e.Message);
+ }
+ });
+
+ return textFile;
+ }
+
+ ///
+ /// Step two: It will likely be necessary to replace certain entries during import, like language, or resource names. This is the place to do that.
+ ///
+ ///
+ /// The adapted textfile or null, if the import should be aborted.
+ private static TextFile AdaptTextFile(BiowareLocalizedStringDatabase textDb, TextFile originalFile)
+ {
+
+ ImportTargetDialog importDialog = new ImportTargetDialog(textDb, originalFile);
+
+ bool? alteredResult = importDialog.ShowDialog();
+ if (alteredResult == true)
+ {
+ return importDialog.SaveValue;
+ }
+
+ App.Logger.Log("Import Aborted");
+ return null;
+
+ }
+
+ ///
+ /// Step three: Actually import the parsed texts. There still might be errors preventing certain texts, but that should only prevent individual texts from importing properly.
+ ///
+ ///
+ ///
+ private static void ImportTexts(BiowareLocalizedStringDatabase textDb, TextFile textFile)
+ {
+
+ FrostyTaskWindow.Show("Importing Custom Texts", "", (task) =>
+ {
+
+ string language = textFile.LanguageFormat;
+
+ try
+ {
+ foreach (TextRepresentation textRepresentation in textFile.Texts)
+ {
+ ImportText(textDb, language, textRepresentation);
+ }
+
+ ImportDeclinatedAdjectives(textDb, language, textFile.DeclinatedAdjectives);
+
+ App.Logger.Log("Texts imported into <{0}>", language);
+ }
+ catch (InvalidOperationException e)
+ {
+ // this is thrown e.g., if the language does not exist in the local game copy
+ App.Logger.LogError("Could not import the texts: {0}", e.Message);
+ }
+
+ });
+ }
+
+ private static void ImportText(BiowareLocalizedStringDatabase textDb, string language, TextRepresentation textRepresentation)
+ {
+
+ string stringIdAsText = textRepresentation.TextId;
+ bool canRead = uint.TryParse(stringIdAsText, NumberStyles.HexNumber, null, out uint textId);
+ if (!canRead)
+ {
+ App.Logger.LogWarning("Text with id <{0}> could not be imported! Text Id cannot be parsed!", stringIdAsText);
+ return;
+ }
+
+ try
+ {
+ textDb.SetText(language, textRepresentation.Resources, textId, textRepresentation.Text);
+ }
+ catch (Exception e)
+ {
+ // ArgumentException or KeyException are thrown if the language or resource do not exist
+ App.Logger.LogError("Text with id <{0}> could not be imported: {1}", stringIdAsText, e.Message);
+ }
+ }
+
+ private static void ImportDeclinatedAdjectives(BiowareLocalizedStringDatabase textDb, string language, DeclinatedAdjectiveRepresentation[] nullableAdjectiveRepresentations)
+ {
+ if (nullableAdjectiveRepresentations != null)
+ {
+ LanguageTextsDB localizedTextDb = textDb.GetLocalizedTextDB(language);
+ foreach (var adjectiveRepresentation in nullableAdjectiveRepresentations)
+ {
+ ImportDeclinatedAdjective(localizedTextDb, adjectiveRepresentation);
+ }
+ }
+ }
+
+ private static void ImportDeclinatedAdjective(LanguageTextsDB localizedTextDb, DeclinatedAdjectiveRepresentation adjectiveRepresentation)
+ {
+
+ string adjectiveIdAsText = adjectiveRepresentation.AdjectiveId;
+ bool canRead = uint.TryParse(adjectiveIdAsText, NumberStyles.HexNumber, null, out uint adjectiveId);
+ if (!canRead)
+ {
+ App.Logger.LogWarning("Adjective with id <{0}> could not be imported! Adjective Id cannot be parsed!", adjectiveIdAsText);
+ return;
+ }
+
+ List declinationsList = new List(adjectiveRepresentation.Declinations);
+
+ try
+ {
+ localizedTextDb.SetDeclinatedAdjectve(adjectiveRepresentation.Resource, adjectiveId, declinationsList);
+ }
+ catch (Exception e)
+ {
+ // this is thrown if the resource does not exist - should not actually happen with the selector dialog before...
+ App.Logger.LogError("Declinated adjective with id <{0}> could not be imported: {1}", adjectiveIdAsText, e.Message);
+ }
+ }
+
+ }
+}
diff --git a/Plugins/BiowareLocalizationPlugin/LocalizedResources/LanguageTextsDB.cs b/Plugins/BiowareLocalizationPlugin/LocalizedResources/LanguageTextsDB.cs
new file mode 100644
index 000000000..fa71c2b7e
--- /dev/null
+++ b/Plugins/BiowareLocalizationPlugin/LocalizedResources/LanguageTextsDB.cs
@@ -0,0 +1,516 @@
+using Frosty.Core;
+using FrostySdk.Managers;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace BiowareLocalizationPlugin.LocalizedResources
+{
+
+ ///
+ /// Most of the actual get/set and find Texts methods per language are located in here. This class then forwards calls to the correct LocalizedStringResource instances.
+ ///
+ public class LanguageTextsDB
+ {
+
+ #region -- HelperClass
+ ///
+ /// Stores information about which resources a text is located in in the vanilla game, and which resources where added by mods.
+ ///
+ private class TextLocation
+ {
+ ///
+ /// The id of the specific text.
+ ///
+ public uint TextId { get; }
+
+ ///
+ /// The resource names with occurences of that text in the vanilla game.
+ ///
+ public ISet DefaultResourceNames { get; }
+
+ ///
+ /// The resource names where a mod added the text to.
+ ///
+ public ISet AddedResourceNames = new HashSet();
+
+ ///
+ /// Constructor
+ ///
+ ///
+ public TextLocation(uint inTextId)
+ {
+ TextId = inTextId;
+ DefaultResourceNames = new HashSet();
+ }
+
+ ///
+ /// Constructor
+ ///
+ ///
+ ///
+ public TextLocation(uint inTextId, string inDefaultResource)
+ {
+ TextId = inTextId;
+ DefaultResourceNames = new HashSet() { inDefaultResource };
+ }
+
+ ///
+ /// Constructor
+ ///
+ ///
+ ///
+ /// /// true if the resourcename belongs to a modified resource, false if it is amodified resource
+ public TextLocation(uint inTextId, string inResourceName, bool inIsAddedResource)
+ {
+ TextId = inTextId;
+ DefaultResourceNames = new HashSet();
+
+ if (inIsAddedResource)
+ {
+ AddedResourceNames.Add(inResourceName);
+ }
+ else
+ {
+ DefaultResourceNames.Add(inResourceName);
+ }
+ }
+
+ ///
+ /// Adds the given resourcename to the list of added resources, if it is not already present.
+ ///
+ ///
+ public void AddLocation(string resourceName)
+ {
+ if (!DefaultResourceNames.Contains(resourceName))
+ {
+ AddedResourceNames.Add(resourceName);
+ }
+ }
+ }
+
+ // Enum specifying which resource location to retrieve for a text id.
+ private enum ResourceLocationRequest { DEFAULT_ONLY, ADDED, ALL };
+ #endregion
+
+ ///
+ /// The name of the language format in the game.
+ ///
+ public string LanguageIdentifier { get; private set; }
+
+ ///
+ /// This dictionary contains all the found text ids as well as their text.
+ ///
+ private readonly Dictionary m_textsForId = new Dictionary();
+
+ ///
+ /// This dictionary contains all resource assets where a string was found.
+ ///
+ private readonly Dictionary m_resourcesForStringId = new Dictionary();
+
+ ///
+ /// The resources available
+ ///
+ private readonly SortedDictionary m_resourcesByName = new SortedDictionary();
+
+ ///
+ /// Lists the names of all resources that include declinated adjectives (both of them for dai, one for mea).
+ ///
+ private readonly SortedSet m_declinatedAdjectiveIncludingResurces = new SortedSet();
+
+ public void Init(string languageName, IEnumerable bundlePaths)
+ {
+ LanguageIdentifier = languageName;
+ LoadTextResources(bundlePaths);
+ }
+
+ ///
+ /// Tries to return the text for the given uid. Returns an error message if the text does not exist.
+ /// @see #FindText
+ ///
+ ///
+ ///
+ public string GetText(uint id)
+ {
+ if (!m_textsForId.ContainsKey(id))
+ {
+ return id == 0 ? "" : string.Format("Invalid StringId: {0}", id.ToString("X8"));
+ }
+ return m_textsForId[id];
+ }
+
+ ///
+ /// Tries to return the text for the given uid, returns null if the textid does not exist.
+ /// @see #GetString
+ ///
+ ///
+ ///
+ public string FindText(uint textId)
+ {
+ bool textExists = m_textsForId.TryGetValue(textId, out string text);
+ return textExists ? text : null;
+ }
+
+ ///
+ /// Returns the list of LocalizedStringResource in which the given text id can be found by default.
+ ///
+ /// The text id to look for.
+ /// All resources in which the text id can be found by default.
+ public IEnumerable GetDefaultResourcesForTextId(uint textId)
+ {
+ bool exists = m_resourcesForStringId.TryGetValue(textId, out TextLocation textLocation);
+ return exists ? GetResources(textLocation, ResourceLocationRequest.DEFAULT_ONLY) : new List();
+ }
+
+ ///
+ /// Returns the list of LocalizedStringResource in which the given text id can be found.
+ ///
+ /// The text id to look for.
+ /// All resources in which the text id can be found.
+ public IEnumerable GetAllResourcesForTextId(uint textId)
+ {
+ bool exists = m_resourcesForStringId.TryGetValue(textId, out TextLocation textLocation);
+ return exists ? GetResources(textLocation, ResourceLocationRequest.ALL) : new List();
+ }
+
+ ///
+ /// Returns the list of LocalizedStringResource in which the given text id was inserted by a mod.
+ ///
+ /// The text id to look for.
+ /// All non default resources in which the text id can be found.
+ public IEnumerable GetAddedResourcesForTextId(uint textId)
+ {
+ bool exists = m_resourcesForStringId.TryGetValue(textId, out TextLocation textLocation);
+ return exists ? GetResources(textLocation, ResourceLocationRequest.ADDED) : new List();
+ }
+
+ ///
+ /// Returns the names of all found resources
+ ///
+ ///
+ public IEnumerable GetAllResourceNames()
+ {
+ return m_resourcesByName.Keys;
+ }
+
+ ///
+ /// Returns all text ids
+ ///
+ ///
+ public IEnumerable GetAllTextIds()
+ {
+ foreach (uint key in m_textsForId.Keys)
+ {
+ yield return key;
+ }
+ }
+
+ public IEnumerable GetAllModifiedTextsIds()
+ {
+ HashSet modifiedTextIds = new HashSet();
+
+ foreach (LocalizedStringResource resource in m_resourcesByName.Values)
+ {
+ modifiedTextIds.UnionWith(resource.GetAllModifiedTextsIds());
+ }
+
+ return modifiedTextIds;
+ }
+
+ ///
+ /// Sets a text into a single resource.
+ ///
+ ///
+ ///
+ ///
+ public void SetText(string resourceName, uint textId, string text)
+ {
+
+ LocalizedStringResource resource = GetResourceByName(resourceName);
+
+ resource.SetText(textId, text);
+
+ bool locExists = m_resourcesForStringId.TryGetValue(textId, out TextLocation textLocation);
+ if (!locExists)
+ {
+ textLocation = new TextLocation(textId);
+ m_resourcesForStringId.Add(textId, textLocation);
+ }
+ textLocation.AddLocation(resourceName);
+ }
+
+ ///
+ /// Removes a single text from a modified resource.
+ ///
+ ///
+ ///
+ public void RemoveText(string resourceName, uint textId)
+ {
+ LocalizedStringResource resource = GetResourceByName(resourceName);
+ resource.RemoveText(textId);
+
+ bool textLocationExists = m_resourcesForStringId.TryGetValue(textId, out TextLocation textLocation);
+ if (!textLocationExists)
+ {
+ App.Logger.LogError("TextID <{0}> does not exist for language <{1}>!", textId, LanguageIdentifier);
+ return;
+ }
+
+ textLocation.AddedResourceNames.Remove(resourceName);
+
+ if (textLocation.AddedResourceNames.Count == 0 && textLocation.DefaultResourceNames.Count == 0)
+ {
+ m_resourcesForStringId.Remove(textId);
+ }
+ }
+
+ ///
+ /// Removes a text from the textDB's cache. This usually is only a intermediary operation, until the updated text is reinserted.
+ ///
+ ///
+ public void RemoveTextFromCache(uint textId)
+ {
+ m_textsForId.Remove(textId);
+ }
+
+ ///
+ /// Updates the textDb's cache with the new value for the text id.
+ ///
+ ///
+ ///
+ public void UpdateTextCache(uint textId, string text)
+ {
+ m_textsForId[textId] = text;
+ }
+
+ public void RevertText(uint textId)
+ {
+ string defaultText = null;
+ ISet nonDefaultResourceNames = m_resourcesForStringId[textId].AddedResourceNames;
+ var resources = new List(GetAllResourcesForTextId(textId));
+
+ foreach (LocalizedStringResource resource in resources)
+ {
+ if (defaultText == null)
+ {
+ defaultText = resource.GetDefaultText(textId);
+ }
+ resource.RemoveText(textId);
+ nonDefaultResourceNames.Remove(resource.Name);
+ }
+
+ if (defaultText != null)
+ {
+ UpdateTextCache(textId, defaultText);
+ }
+ else
+ {
+ RemoveTextFromCache(textId);
+ }
+ }
+
+ public IEnumerable GetAllResourceNamesWithDeclinatedAdjectives()
+ {
+ return m_declinatedAdjectiveIncludingResurces;
+ }
+
+ public IEnumerable GetAllResourceNamesWithModifiedDeclinatedAdjectives()
+ {
+ foreach (string resourceName in m_declinatedAdjectiveIncludingResurces)
+ {
+ LocalizedStringResource resource = GetResourceByName(resourceName);
+
+ if (resource.ContainsModifiedDeclinatedAdjectives())
+ {
+ yield return resourceName;
+ }
+ }
+ }
+
+ public IEnumerable GetAllDeclinatedAdjectiveIdsFromResource(string resourceName)
+ {
+ LocalizedStringResource resource = GetResourceByName(resourceName);
+ return resource.GetAllDeclinatedAdjectivesIds();
+ }
+
+ public IEnumerable GetModifiedDeclinatedAdjectiveIdsFromResource(string resourceName)
+ {
+ LocalizedStringResource resource = GetResourceByName(resourceName);
+ return resource.GetAllModifiedDeclinatedAdjectivesIds();
+ }
+
+ public List GetDeclinatedAdjectives(string resourceName, uint adjectiveId)
+ {
+ LocalizedStringResource resource = GetResourceByName(resourceName);
+ return resource.GetDeclinatedAdjective(adjectiveId);
+ }
+
+ public void SetDeclinatedAdjectve(string resourceName, uint adjectiveId, List aAdjectives)
+ {
+ LocalizedStringResource resource = GetResourceByName(resourceName);
+ resource.SetAdjectiveDeclinations(adjectiveId, aAdjectives);
+
+ m_declinatedAdjectiveIncludingResurces.Add(resourceName);
+ }
+
+ public void RevertDeclinatedAdjective(string resourceName, uint adjectiveId)
+ {
+ LocalizedStringResource resource = GetResourceByName(resourceName);
+ resource.RemoveAdjectiveDeclination(adjectiveId);
+
+ if (!resource.ContainsDeclinatedAdjectives())
+ {
+ m_declinatedAdjectiveIncludingResurces.Remove(resourceName);
+ }
+ }
+
+ public IEnumerable GetAllTextIdsFromResource(string resourceName)
+ {
+ LocalizedStringResource resource = GetResourceByName(resourceName);
+ var entries = resource.GetAllPrimaryTexts();
+ return entries.Select(tuple => tuple.Item1);
+ }
+
+ public IEnumerable GetAllModifiedTextIdsFromResource(string resourceName)
+ {
+ LocalizedStringResource resource = GetResourceByName(resourceName);
+ return resource.GetAllModifiedTextsIds();
+ }
+
+ ///
+ /// Loads the localizedSTringResources from the given budles.
+ ///
+ ///
+ private void LoadTextResources(IEnumerable bundlePaths)
+ {
+
+ foreach (string superBundlePathPart in bundlePaths)
+ {
+ string superBundlePath = "win32/" + superBundlePathPart.ToLowerInvariant();
+ foreach (ResAssetEntry resEntry in App.AssetManager.EnumerateRes(resType: (uint)ResourceType.LocalizedStringResource, bundleSubPath: superBundlePath))
+ {
+
+ LocalizedStringResource resource = App.AssetManager.GetResAs(resEntry);
+ if (resource != null)
+ {
+
+ m_resourcesByName[resource.Name] = resource;
+ FetchResourceTexts(resource);
+
+ resource.ResourceEventHandlers += (s, e) =>
+ {
+ UpdateResourceTexts((LocalizedStringResource)s);
+ };
+ }
+ }
+ }
+ }
+
+ ///
+ /// Gets all texts in the resource and updates the caches.
+ ///
+ ///
+ private void FetchResourceTexts(LocalizedStringResource resource)
+ {
+
+ string resourceName = resource.Name;
+ foreach (var entry in resource.GetAllPrimaryTexts())
+ {
+ uint textId = entry.Item1;
+ bool isModifiedText = entry.Item3;
+
+ m_textsForId[textId] = entry.Item2;
+
+ bool textLocationExists = m_resourcesForStringId.TryGetValue(textId, out TextLocation textlocation);
+
+ if (!textLocationExists)
+ {
+ textlocation = new TextLocation(textId, resourceName, isModifiedText);
+ m_resourcesForStringId.Add(textId, textlocation);
+ }
+ else if (isModifiedText)
+ {
+ textlocation.AddLocation(resourceName);
+ }
+ else
+ {
+ textlocation.DefaultResourceNames.Add(resourceName);
+ }
+ }
+
+ if (resource.ContainsDeclinatedAdjectives())
+ {
+ m_declinatedAdjectiveIncludingResurces.Add(resourceName);
+ }
+ }
+
+ private void UpdateResourceTexts(LocalizedStringResource resource)
+ {
+
+ // need to remove all texts wiht only resource as location...
+ string resourceName = resource.Name;
+
+ // remove via explizit loop, too tired to figure out another way
+ List textIdsToRemove = new List();
+ foreach (TextLocation textloc in m_resourcesForStringId.Values)
+ {
+ if (textloc.AddedResourceNames.Remove(resourceName) && textloc.DefaultResourceNames.Count == 0)
+ {
+ if (textloc.AddedResourceNames.Count == 0)
+ {
+ textIdsToRemove.Add(textloc.TextId);
+ }
+ }
+ }
+ foreach (uint toRemove in textIdsToRemove)
+ {
+ m_resourcesForStringId.Remove(toRemove);
+ m_textsForId.Remove(toRemove);
+ }
+
+ FetchResourceTexts(resource);
+ }
+
+ ///
+ /// Returns all resources from the given TextLocation that match the given resourceLocation- variant.
+ ///
+ ///
+ ///
+ ///
+ private IEnumerable GetResources(TextLocation textLocation, ResourceLocationRequest resourceLocations)
+ {
+ if (ResourceLocationRequest.ALL == resourceLocations || ResourceLocationRequest.DEFAULT_ONLY == resourceLocations)
+ {
+ foreach (string resourceName in textLocation.DefaultResourceNames)
+ {
+ yield return m_resourcesByName[resourceName];
+ }
+ }
+
+ if (ResourceLocationRequest.ALL == resourceLocations || ResourceLocationRequest.ADDED == resourceLocations)
+ {
+ foreach (string addedResourceName in textLocation.AddedResourceNames)
+ {
+ yield return m_resourcesByName[addedResourceName];
+ }
+ }
+ }
+
+ ///
+ /// Returns the resource of the given name, or throws an exception if the resource does not exist.
+ ///
+ ///
+ ///
+ ///
+ private LocalizedStringResource GetResourceByName(string resourceName)
+ {
+ bool resourceExists = m_resourcesByName.TryGetValue(resourceName, out LocalizedStringResource resource);
+ if (!resourceExists)
+ {
+ throw new InvalidOperationException(string.Format("Resource of name <{0}> does not exist for language <{1}>!", resourceName, LanguageIdentifier));
+ }
+
+ return resource;
+ }
+ }
+}
diff --git a/Plugins/BiowareLocalizationPlugin/LocalizedResources/LocalizedStringResource.cs b/Plugins/BiowareLocalizationPlugin/LocalizedResources/LocalizedStringResource.cs
new file mode 100644
index 000000000..61e73fe65
--- /dev/null
+++ b/Plugins/BiowareLocalizationPlugin/LocalizedResources/LocalizedStringResource.cs
@@ -0,0 +1,1404 @@
+using Frosty.Core;
+using FrostySdk;
+using FrostySdk.IO;
+using FrostySdk.Managers;
+using FrostySdk.Resources;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using System.Linq;
+using System;
+
+namespace BiowareLocalizationPlugin.LocalizedResources
+{
+
+ public class LocalizedStringResource : Resource
+ {
+
+ ///
+ /// The (display) name of the resource this belongs to
+ ///
+ public string Name { get; private set; } = "not_yet_initialized";
+
+ ///
+ /// Event handler to be informed whenever the state of the modified resource changes drastically.
+ ///
+ public event EventHandler ResourceEventHandlers;
+
+ ///
+ /// Toggle to enable / disable further debug log messages -Remember to turn this off before release!
+ ///
+ private static readonly bool m_printVerificationTexts = false;
+
+ ///
+ /// How to handle incorrect metadata offsets in the resource header.
+ ///
+ private static readonly PositionOffsetErrorHandling m_ContinueAfterOffsetErrorVariant = PositionOffsetErrorHandling.HEADER_DATAOFFSET;
+
+ ///
+ /// The default texts
+ ///
+ private readonly List m_localizedStrings = new List();
+
+ ///
+ /// Lists all the string ids that are stored at the key position
+ ///
+ private readonly Dictionary> m_stringIdsAtPositionOffset = new Dictionary>();
+
+ ///
+ /// Possible variants of where to position the reader in the case that the stated data offset does not match the data in the resource.
+ /// None of these actually work, in the case of discrepancy you are screwed anyway :(
+ ///
+ private enum PositionOffsetErrorHandling
+ {
+ ///
+ /// Use the current reader position going forward
+ ///
+ READER_POSITON,
+
+ ///
+ /// Set reader to the position stated in the header
+ ///
+ HEADER_DATAOFFSET,
+
+ ///
+ /// Set the reader to the position stated in the metadata
+ ///
+ METADATA_DATAOFFSET
+ }
+
+ ///
+ /// List of supported characters, ordered by their position within the node list, i.e., their frequency within all texts
+ ///
+ private List m_supportedCharacters = new List();
+
+ ///
+ /// If any text is altered, the altered text entry will be kept in the modfiedResource.
+ ///
+ private ModifiedLocalizationResource m_modifiedResource = null;
+
+ ///
+ /// The header information of the localization resource.
+ ///
+ private ResourceHeader m_headerData;
+
+ ///
+ /// The default encoding's root node. Note that the rootNode itself is not part of the serialized data!
+ ///
+ private HuffmanNode m_encodingRootNode;
+
+ ///
+ /// Byte array of currently unknown data packed between the header list of string positions, and the actual text entries.
+ ///
+ private List m_unknownData;
+
+ ///
+ /// Ids and texts of Declinated adjectives for creafted items in DA:I
+ /// This has internal access only for the test utils
+ ///
+ internal DragonAgeDeclinatedAdjectiveTuples DragonAgeDeclinatedCraftingNames { get; private set; }
+
+ // TODO this currently stores a lot of redundant information, clean up at a later stage!
+
+ public LocalizedStringResource()
+ {
+ // nothing to do?!
+ }
+
+ public override void Read(NativeReader reader, AssetManager am, ResAssetEntry entry, ModifiedResource modifiedData)
+ {
+
+ base.Read(reader, am, entry, modifiedData);
+
+ Name = new StringBuilder(entry.Filename)
+ .Append(" - ")
+ .Append(entry.Name)
+ .ToString();
+
+ if (ProfilesLibrary.DataVersion == (int)ProfileVersion.Anthem)
+ {
+ ReadAnthemStrings(reader, entry);
+ }
+ else
+ {
+ // Profile Version MEA = 20170321
+ // Profile Version DAI = 20141118
+ Read_MassEffect_DragonAge_Strings(reader);
+ }
+
+ m_modifiedResource = modifiedData as ModifiedLocalizationResource;
+ if (m_modifiedResource != null)
+ {
+ m_modifiedResource.InitResourceId(resRid);
+ }
+
+ // keep informed about changes...
+ entry.AssetModified += (s, e) => OnModified((ResAssetEntry)s);
+ }
+
+ public override byte[] SaveBytes()
+ {
+
+ // remove these logs
+ if (m_printVerificationTexts) { App.Logger.Log("Writing Texts for <{0}>", Name); }
+
+ /*Plan of Action:
+ * -recalculate Huffman encoding
+ * -- getHuffman codes for each character
+ * -recompute list of string ids and string positions with altered texts
+ * -- need to already encoded strings for the position calculation!
+ * -recompute other offsets based on the last step info
+ * -replace the meta data with the new dataOffset (!)
+ * -write header with altered offsets
+ * -write stuff and things between header and strings
+ * -write strings with the new huffman encoding
+ */
+
+ List> allTexts = GetAllSortedTextsToWrite();
+ HuffmanNode newRootNode = GetEncodingRootNode(allTexts);
+
+ uint nodeOffset = m_headerData.NodeOffset;
+
+ // flatten the tree, we need to list representation again...
+ List nodeList = ResourceUtils.GetNodeListToWrite(newRootNode);
+ uint newNodeCount = (uint)nodeList.Count;
+
+ uint encodingNodesSize = newNodeCount * 4;
+ uint newStringsOffset = nodeOffset + encodingNodesSize;
+
+ Dictionary> encoding = ResourceUtils.GetCharEncoding(nodeList);
+ EncodedTextPositionGrouping encodedTextsGrouping = ResourceUtils.GetEncodedTextsToWrite(allTexts, encoding);
+
+ uint newStringsCount = (uint)encodedTextsGrouping.PrimaryTextIdsAndPositions.Count;
+
+ uint blockOffset = newStringsOffset + (newStringsCount * 8);
+ uint lastBlockSize = 0;
+ List recalculatedAdditionalOffsets = new List();
+
+ // one for the money: No idea what this is
+ foreach (DataCountAndOffsets unknownDef in m_headerData.FirstUnknownDataDefSegments)
+ {
+ uint byteBlockCount8 = unknownDef.Count;
+ blockOffset += lastBlockSize;
+ recalculatedAdditionalOffsets.Add(new DataCountAndOffsets()
+ {
+ Count = byteBlockCount8,
+ Offset = blockOffset
+
+ });
+
+ lastBlockSize = byteBlockCount8 * 8;
+ }
+
+ // two for the show: These are the ids and positions of the declinated adjectives used in DA:I crafting
+ foreach (var declinatedAdjectivesBlock in encodedTextsGrouping.DeclinatedAdjectivesIdsAndPositions)
+ {
+ uint byteBlockCount8 = (uint)declinatedAdjectivesBlock.Count;
+ blockOffset += lastBlockSize;
+ recalculatedAdditionalOffsets.Add(new DataCountAndOffsets()
+ {
+ Count = byteBlockCount8,
+ Offset = blockOffset
+
+ });
+ lastBlockSize = byteBlockCount8 * 8;
+ }
+
+ uint newDataOffset = blockOffset + lastBlockSize;
+ // replace the metadata which is where the game actually reads the dataoffset from.
+ ReplaceMetaData(newDataOffset);
+
+ using (NativeWriter writer = new NativeWriter(new MemoryStream()))
+ {
+
+ // Then write the type dependent header data
+ writer.Write(ResourceHeader.Magic);
+
+ writer.Write(m_headerData.Unknown1);
+
+ writer.Write(newDataOffset);
+
+ writer.Write(m_headerData.Unknown2);
+ writer.Write(m_headerData.Unknown3);
+ writer.Write(m_headerData.Unknown4);
+
+ writer.Write(newNodeCount);
+ writer.Write(nodeOffset);
+ writer.Write(newStringsCount);
+ writer.Write(newStringsOffset);
+
+ foreach (DataCountAndOffsets uds in recalculatedAdditionalOffsets)
+ {
+ writer.Write(uds.Count);
+ writer.Write(uds.Offset);
+ }
+
+ if (m_printVerificationTexts)
+ { App.Logger.Log(".. Writer Position before <{0}> nodes is <{1}>, expected <{2}> ", nodeList.Count, writer.Position, nodeOffset); }
+
+ // Write huffman nodes
+ foreach (HuffmanNode node in nodeList)
+ {
+ writer.Write(node.Value);
+ }
+
+ long actualStringsOffset = writer.Position;
+ if (m_printVerificationTexts)
+ { App.Logger.Log(".. Writer Position before textlocations is <{0}>, expected <{1}> ", writer.Position, newStringsOffset); }
+
+ //Write string id positions
+ foreach (KeyValuePair entry in encodedTextsGrouping.PrimaryTextIdsAndPositions)
+ {
+ writer.Write(entry.Key);
+ writer.Write(entry.Value.Position);
+ }
+
+ if (m_printVerificationTexts)
+ {
+ App.Logger.Log(".. Writer Position after <{0}> textlocations is <{1}>, expected <{2}>. Length of last part was <{3}>",
+ encodedTextsGrouping.PrimaryTextIdsAndPositions.Count, writer.Position, recalculatedAdditionalOffsets[0].Offset, writer.Position - actualStringsOffset);
+ }
+
+ //Write unknownDataSegments
+ foreach (byte[] someData in m_unknownData)
+ {
+ writer.Write(someData);
+ }
+
+ // write the ids and positions of the declinated adjectives.
+ foreach (var declinationBlock in encodedTextsGrouping.DeclinatedAdjectivesIdsAndPositions)
+ {
+ foreach (KeyValuePair entry in declinationBlock)
+ {
+ writer.Write(entry.Key);
+ writer.Write(entry.Value.Position);
+ }
+ }
+
+ if (m_printVerificationTexts)
+ { App.Logger.Log(".. Writer Position before texts is <{0}>, expected <{1}>", writer.Position, newDataOffset); }
+
+ // Write encoded texts
+ byte[] bitTexts = ResourceUtils.GetTextRepresentationToWrite(encodedTextsGrouping.AllEncodedTextPositions);
+ writer.Write(bitTexts);
+
+ if (m_printVerificationTexts)
+ {
+ App.Logger.Log(".. Writer Position after encoded texts is <{0}>. EncodedTexts size was <{1}> byte", writer.Position, bitTexts.Length);
+ }
+
+ return writer.ToByteArray();
+ }
+ }
+
+ public override ModifiedResource SaveModifiedResource()
+ {
+ return m_modifiedResource;
+ }
+
+ ///
+ /// Adds or edits the text of the given id.
+ ///
+ ///
+ ///
+ public void SetText(uint textId, string text)
+ {
+
+ // Try to revert if text equals original
+ // -> drawback is long iteration over all texts or another huge instance of textid to text dictionary :(
+
+ // have to try anyway as long as no dedicated remove is present..
+ foreach (var entry in m_localizedStrings)
+ {
+ if (textId == entry.Id)
+ {
+ // found the right one
+ // neither the entryValue nor the given text can be null
+ if (entry.Value.Equals(text))
+ {
+ // It is the original text, remove instead
+ RemoveText(textId);
+ return;
+ }
+ break;
+ }
+ }
+
+ SetText0(textId, text);
+ }
+
+ public void RemoveText(uint textId)
+ {
+ if (m_modifiedResource != null)
+ {
+ m_modifiedResource.RemoveText(textId);
+
+ ModifyResourceAfterDelete();
+ }
+ }
+
+ public void SetAdjectiveDeclinations(uint adjectiveId, List declinations)
+ {
+ List defaultDeclinations = DragonAgeDeclinatedCraftingNames.GetDeclinatedAdjective(adjectiveId).ToList();
+
+ bool shouldSet = AreDeclinationsDifferentFromDefault(defaultDeclinations, declinations);
+
+ if (shouldSet)
+ {
+ SetAdjectiveDeclinations0(adjectiveId, declinations);
+ }
+ else
+ {
+ RemoveAdjectiveDeclination(adjectiveId);
+ }
+ }
+
+ public void RemoveAdjectiveDeclination(uint adjectiveId)
+ {
+ if (m_modifiedResource != null)
+ {
+ m_modifiedResource.RemoveDeclinatedCraftingAdjective(adjectiveId);
+ ModifyResourceAfterDelete();
+ }
+ }
+
+ public string GetDefaultText(uint textId)
+ {
+ // FIXME hopefully this isn't used often...
+ foreach (var entry in m_localizedStrings)
+ {
+ if (textId == entry.Id)
+ {
+ return entry.Value;
+ }
+ }
+ return null;
+ }
+
+ public IEnumerable GetAllModifiedTextsIds()
+ {
+ if (m_modifiedResource == null)
+ {
+ return new List();
+ }
+
+ return new List(m_modifiedResource.AlteredTexts.Keys);
+ }
+
+ ///
+ /// Returns the ids of all default declinated adjectives in this resource
+ ///
+ ///
+ public IEnumerable GetAllDefaultDeclinatedAdjectivesIds()
+ {
+ return DragonAgeDeclinatedCraftingNames.GetDeclinatedAdjectiveIds();
+ }
+
+ ///
+ /// Returns only the ids of the declinated adjectivs altered by a mod.
+ ///
+ ///
+ public IEnumerable GetAllModifiedDeclinatedAdjectivesIds()
+ {
+ if (m_modifiedResource != null)
+ {
+ return m_modifiedResource.AlteredDeclinatedCraftingAdjectives.Keys;
+ }
+
+ return new uint[0];
+ }
+
+ ///
+ /// Returns the set of all adjective ids in this resource.
+ ///
+ ///
+ public IEnumerable GetAllDeclinatedAdjectivesIds()
+ {
+ SortedSet adjectiveIds = new SortedSet();
+
+ adjectiveIds.UnionWith(GetAllDefaultDeclinatedAdjectivesIds());
+ adjectiveIds.UnionWith(GetAllModifiedDeclinatedAdjectivesIds());
+
+ return adjectiveIds;
+ }
+
+ ///
+ /// Returns the list of declinations for a single default adjective.
+ ///
+ ///
+ ///
+ public List GetDefaultDeclinatedAdjective(uint adjectiveId)
+ {
+ List adjectiveStrings = new List(DragonAgeDeclinatedCraftingNames.NumberOfDeclinations);
+
+ int i = 0;
+ foreach (LocalizedString entry in DragonAgeDeclinatedCraftingNames.GetDeclinatedAdjective(adjectiveId))
+ {
+ if (entry != null)
+ {
+ adjectiveStrings.Insert(i, entry.Value);
+ }
+ i++;
+ }
+
+ return adjectiveStrings;
+ }
+
+ ///
+ /// Returns the list of declinations for a modified adjective.
+ ///
+ ///
+ ///
+ public List GetDeclinatedAdjective(uint adjectiveId)
+ {
+
+ if (m_modifiedResource != null)
+ {
+ bool modifiedExists = m_modifiedResource.AlteredDeclinatedCraftingAdjectives.TryGetValue(adjectiveId, out List output);
+ if (modifiedExists)
+ {
+ return output;
+ }
+ }
+
+ return GetDefaultDeclinatedAdjective(adjectiveId);
+ }
+
+ ///
+ /// Returns whether or not there are declinated adjectives included in this resource.
+ ///
+ ///
+ public bool ContainsDeclinatedAdjectives()
+ {
+
+ bool modifiedAdjectivesExist = ContainsModifiedDeclinatedAdjectives();
+ return DragonAgeDeclinatedCraftingNames.ContainsAdjectives || modifiedAdjectivesExist;
+ }
+
+ ///
+ /// Returns true if this resource contains modified declinated crafting adjectives.
+ ///
+ ///
+ public bool ContainsModifiedDeclinatedAdjectives()
+ {
+
+ if (m_modifiedResource != null)
+ {
+ return m_modifiedResource.AlteredDeclinatedCraftingAdjectives.Count > 0;
+ }
+ return false;
+ }
+
+ ///
+ /// Reads a single text from the given bit reader's current position
+ ///
+ ///
+ ///
+ /// text
+ private static string ReadSingleText(BitReader bitReader, HuffmanNode rootNode)
+ {
+ StringBuilder sb = new StringBuilder();
+ while (true)
+ {
+ HuffmanNode n = rootNode;
+ while (n.Left != null && n.Right != null && !bitReader.EndOfStream)
+ {
+ bool b = bitReader.GetBit();
+ if (b) n = n.Right;
+ else n = n.Left;
+ }
+
+ if (n.Letter == 0x00 || bitReader.EndOfStream)
+ {
+ return sb.ToString();
+ }
+ // else
+ sb.Append(n.Letter);
+ }
+ }
+
+ private static IEnumerable GetAllStringsIdsAsHex(List allStringIds)
+ {
+ foreach (uint stringId in allStringIds)
+ {
+ yield return stringId.ToString("X8");
+ }
+ }
+
+ ///
+ /// Returns all the characters supported by this resource by default.
+ ///
+ /// List of chars.
+ public IEnumerable GetDefaultSupportedCharacters()
+ {
+ return m_supportedCharacters;
+ }
+
+ ///
+ /// Iterates over all (primary) texts in this resource and returns them in the tuple as follows:
+ /// Tuple#0: text id (uint)
+ /// Tuple#1: text (string)
+ /// Tuple#2: whether the text is modified or default (bool). True if modified.
+ /// The same text can be encountered twice in this enumeration, first in its vanilla state, and then the modified version.
+ /// Note: This method does *NOT* return the texts referenced in the 8ByteBlockData between the StringData and the Strings parts of the resource.
+ ///
+ /// An enumerable (stream) of the primary text entries
+ public IEnumerable> GetAllPrimaryTexts()
+ {
+ for (int i = 0; i < m_localizedStrings.Count; i++)
+ {
+ yield return new Tuple(m_localizedStrings[i].Id, m_localizedStrings[i].Value, false);
+ }
+
+ if (m_modifiedResource != null)
+ {
+ foreach (KeyValuePair modifiedEntry in m_modifiedResource.AlteredTexts)
+ {
+ yield return new Tuple(modifiedEntry.Key, modifiedEntry.Value, true);
+ }
+ }
+ }
+
+ ///
+ /// Returns the list of all other vanilla string ids (as hex representation) that share the same text and position.
+ /// To Reiterate: This only lists vanilla unmodified strings and positions!
+ ///
+ ///
+ /// Enumeration of hexadecimal string ids
+ public IEnumerable GetAllTextIdsAtPositionOf(uint textId)
+ {
+
+ LocalizedStringWithId textEntry = null;
+ foreach (LocalizedStringWithId searchTextEntry in m_localizedStrings)
+ {
+ if (searchTextEntry.Id == textId)
+ {
+ textEntry = searchTextEntry;
+ break;
+ }
+ }
+
+ if (textEntry != null && (textEntry.DefaultPosition >= 0))
+ {
+ bool valuePresent = m_stringIdsAtPositionOffset.TryGetValue(textEntry.DefaultPosition, out List allStringIds);
+ if (valuePresent)
+ {
+ return GetAllStringsIdsAsHex(allStringIds);
+ }
+ }
+ return Enumerable.Empty();
+ }
+
+ // This is only here for test purposes!
+ public HuffmanNode GetRootNode()
+ {
+ return m_encodingRootNode;
+ }
+
+ ///
+ /// Returns a list of tuples with the id and bit offset for declinated adjectives used when crafting items in DA:I
+ ///
+ ///
+ ///
+ ///
+ ///
+ private static List ReadDragonAgeDeclinatedItemNamePartIdsAndOffsets(NativeReader reader, DataCountAndOffsets countAndOffset)
+ {
+
+ List itemCraftingNameParts = new List();
+ for (int i = 0; i < countAndOffset.Count; i++)
+ {
+ uint textId = reader.ReadUInt();
+ int defaultPosition = reader.ReadInt();
+ LocalizedStringWithId namePartInfo = new LocalizedStringWithId(textId, defaultPosition);
+ itemCraftingNameParts.Add(namePartInfo);
+ }
+
+ if (m_printVerificationTexts && countAndOffset.Count > 0)
+ {
+ App.Logger.Log("... Read <{0}> declinated adjectives in a block", countAndOffset.Count);
+ }
+
+ return itemCraftingNameParts;
+ }
+
+ private void OnModified(ResAssetEntry assetEntry)
+ {
+ // There is an unhandled edge case here:
+ // When a resource is completely replaced by anoher one in a mod, then this method will not pick that up!
+
+ ModifiedAssetEntry modifiedAsset = assetEntry.ModifiedEntry;
+ ModifiedLocalizationResource newModifiedResource = modifiedAsset?.DataObject as ModifiedLocalizationResource;
+
+ if (m_printVerificationTexts)
+ {
+ App.Logger.Log("Asset <{0}> entered onModified", assetEntry.DisplayName);
+ }
+
+ if (newModifiedResource != m_modifiedResource)
+ {
+ m_modifiedResource = newModifiedResource;
+ ResourceEventHandlers?.Invoke(this, new EventArgs());
+ }
+
+ // revert the metadata just in case
+ ReplaceMetaData(m_headerData.DataOffset);
+ }
+
+ ///
+ /// Fills the String list for Anthem.
+ ///
+ /// the data reader.
+ /// the res asset.
+ private void ReadAnthemStrings(NativeReader reader, ResAssetEntry entry)
+ {
+ _ = reader.ReadUInt();
+ _ = reader.ReadUInt();
+ _ = reader.ReadUInt();
+
+ // initialize these, so there is no accidental crash in anthem
+ m_headerData = new ResourceHeader();
+ m_unknownData = new List();
+ DragonAgeDeclinatedCraftingNames = new DragonAgeDeclinatedAdjectiveTuples(0);
+
+ long numStrings = reader.ReadLong();
+ reader.Position += 0x18;
+
+ Dictionary> hashToStringIdMapping = new Dictionary>();
+
+ for (int i = 0; i < numStrings; i++)
+ {
+ uint hash = reader.ReadUInt();
+ uint stringId = reader.ReadUInt();
+ reader.Position += 8;
+ if (!hashToStringIdMapping.ContainsKey(hash))
+ hashToStringIdMapping.Add(hash, new List());
+ hashToStringIdMapping[hash].Add(stringId);
+ }
+
+ reader.Position += 0x18;
+
+ while (reader.Position < reader.Length)
+ {
+ uint hash = reader.ReadUInt();
+ int stringLen = reader.ReadInt();
+ string str = reader.ReadSizedString(stringLen);
+ int stringPosition = (int)reader.Position; // anthem is not really supported anyways...
+
+ if (hashToStringIdMapping.ContainsKey(hash))
+ {
+ foreach (uint stringId in hashToStringIdMapping[hash])
+ m_localizedStrings.Add(new LocalizedStringWithId(stringId, stringPosition, str));
+ }
+ else
+ {
+ App.Logger.Log("Cannot find {0} in {1}", hash.ToString("x8"), entry.Name);
+ }
+ }
+ }
+
+ ///
+ /// Creates the localized string list from the huffman encoded Mass Effect and Dragon Age bundle entries.
+ ///
+ ///
+ private void Read_MassEffect_DragonAge_Strings(NativeReader reader)
+ {
+
+ m_headerData = ResourceUtils.ReadHeader(reader);
+
+ if (m_printVerificationTexts)
+ {
+ App.Logger.Log("Read header data for <{0}>: {1}", Name, m_headerData.ToString());
+ }
+
+ // position of huffman nodes is header.nodeOffset
+ PositionSanityCheck(reader, m_headerData.NodeOffset, "Header");
+ m_encodingRootNode = ResourceUtils.ReadNodes(reader, m_headerData.NodeCount, out List leafCharacters);
+ m_supportedCharacters = leafCharacters;
+
+ // position of string id and position is right after huffman nodes: header.stringsOffset
+ PositionSanityCheck(reader, m_headerData.StringsOffset, "HuffmanCoding");
+ ReadStringData(reader, m_headerData.StringsCount);
+
+ // position after string data is the start of header.unknownDataDef[0].offset
+ PositionSanityCheck(reader, m_headerData.FirstUnknownDataDefSegments[0].Offset, "StringData");
+ m_unknownData = new List();
+ foreach (DataCountAndOffsets dataCountAndOffset in m_headerData.FirstUnknownDataDefSegments)
+ {
+ m_unknownData.Add(ResourceUtils.ReadUnkownSegment(reader, dataCountAndOffset));
+ }
+
+ DragonAgeDeclinatedCraftingNames = new DragonAgeDeclinatedAdjectiveTuples(m_headerData.MaxDeclinations);
+
+ for (int i = 0; i < m_headerData.DragonAgeDeclinatedCraftingNamePartsCountAndOffset.Count; i++)
+ {
+ DataCountAndOffsets dataCountAndOffset = m_headerData.DragonAgeDeclinatedCraftingNamePartsCountAndOffset[i];
+
+ List declinatedAdjectives = ReadDragonAgeDeclinatedItemNamePartIdsAndOffsets(reader, dataCountAndOffset);
+
+ DragonAgeDeclinatedCraftingNames.AddAllAdjectiveForDeclination(declinatedAdjectives, i);
+ }
+
+ DataOffsetReaderPositionSanityCheck(reader);
+
+
+ ReadStrings(reader, m_encodingRootNode, GetAllLocalizedStrings());
+ }
+
+ ///
+ /// Returns the list of all LocalizedString entries found in this resource.
+ /// This list is being comprised of the main texts with unique ids,
+ /// and the set of declinated adjective strings used in dragon age, which all share the same id for several declinations of a word.
+ ///
+ ///
+ private List GetAllLocalizedStrings()
+ {
+ List allLocalizedStrings = new List(m_localizedStrings);
+
+ allLocalizedStrings.AddRange(DragonAgeDeclinatedCraftingNames.GetAllDeclinatedAdjectiveTextLocations());
+
+ return allLocalizedStrings;
+ }
+
+ ///
+ /// Sets the reader to the expected position, if the current position is somewhere else
+ ///
+ ///
+ ///
+ /// the name of the position as used in the warning message
+ private void PositionSanityCheck(NativeReader reader, uint expectedPosition, string positionName)
+ {
+ if (reader.Position != expectedPosition)
+ {
+ App.Logger.LogWarning("Reader for resource <{0}> is at the wrong position after {1}!", Name, positionName);
+ }
+ }
+
+ ///
+ /// Reads the tuples of string id and string bit offset from the dataOffset.
+ /// I.e., it fills the given int array with the offset for the string id, creates an empty _strings entry for the string id
+ /// and also creates a dictionary with the information read from the list.
+ ///
+ /// Note that several String Ids may point towards the same string position!
+ /// Also note that the strings seem to be ordered by their ID, smallest to largest
+ ///
+ ///
+ ///
+ private void ReadStringData(NativeReader reader, uint stringsCount)
+ {
+ for (int i = 0; i < stringsCount; i++)
+ {
+ uint stringId = reader.ReadUInt();
+ int positionOffset = reader.ReadInt();
+
+ m_localizedStrings.Add(new LocalizedStringWithId(stringId, positionOffset));
+
+ // memorize which ids are all stored at the same position
+ List idList;
+ if (!m_stringIdsAtPositionOffset.ContainsKey(positionOffset))
+ {
+ idList = new List();
+ m_stringIdsAtPositionOffset.Add(positionOffset, idList);
+ }
+ else
+ {
+ idList = m_stringIdsAtPositionOffset[positionOffset];
+ }
+ idList.Add(stringId);
+
+ }
+ }
+
+ ///
+ /// Checks that the current position matches the offset as given in the header or at least the metadata.
+ /// If it does not match, the given reader's position is updated to the value in the metadata.
+ ///
+ ///
+ private void DataOffsetReaderPositionSanityCheck(NativeReader reader)
+ {
+
+ uint currentPosition = (uint)reader.Position;
+ uint dataOffsetFromHeader = m_headerData.DataOffset;
+ if (currentPosition != dataOffsetFromHeader)
+ {
+
+ uint dataOffsetFromMeta = BitConverter.ToUInt32(resMeta, 0);
+ if (currentPosition == dataOffsetFromMeta)
+ {
+ App.Logger.LogWarning("Header data for for resource <{0}> is incorrect. 8ByteBlockData is stated to end at <{1}>, instead current reader position is <{2}>, as stated in the metadata!", Name, m_headerData.DataOffset, currentPosition);
+ return;
+ }
+
+ string expectedOffsetInsert = (dataOffsetFromHeader == dataOffsetFromMeta) ?
+ dataOffsetFromHeader.ToString() :
+ string.Format("{0} or {1}", dataOffsetFromHeader, dataOffsetFromMeta);
+
+ uint newPosition;
+ string continueWithOffsetText;
+ switch (m_ContinueAfterOffsetErrorVariant)
+ {
+ case PositionOffsetErrorHandling.READER_POSITON:
+ newPosition = currentPosition;
+ continueWithOffsetText = "Continuing with current position";
+ break;
+ case PositionOffsetErrorHandling.HEADER_DATAOFFSET:
+ newPosition = dataOffsetFromHeader;
+ continueWithOffsetText = string.Format("Continuing with stated position from header: <{0}>", newPosition);
+ break;
+ case PositionOffsetErrorHandling.METADATA_DATAOFFSET:
+ newPosition = dataOffsetFromMeta;
+ continueWithOffsetText = string.Format("Continuing with stated position from MetaData: <{0}>", newPosition);
+ break;
+ default:
+ throw new ArgumentException("Unknown PositionOffsetErrorHandling variant " + m_ContinueAfterOffsetErrorVariant);
+ }
+
+ App.Logger.LogWarning("Expected 8ByteBlockData DataSegment for <{0}> to end at <{1}>, instead current reader position is <{2}>! {3}",
+ Name, expectedOffsetInsert, currentPosition, continueWithOffsetText);
+
+ reader.Position = newPosition;
+ }
+ }
+
+ ///
+ /// Reads the actual texts as sequence of huffman encoded characters.
+ /// The texts are read at the positions given in the list of LocalizedString, updating each of the entries afterwards.
+ ///
+ ///
+ ///
+ ///
+ private void ReadStrings(NativeReader reader, HuffmanNode rootNode, List allLocalizedStrings)
+ {
+ byte[] values = reader.ReadBytes((int)(reader.Length - reader.Position));
+ int textLengthInBytes = values.Length;
+
+ using (BitReader bitReader = new BitReader(new MemoryStream(values)))
+ {
+
+ foreach (LocalizedString stringDefinition in allLocalizedStrings)
+ {
+ int bitOffset = stringDefinition.DefaultPosition;
+ string textName = stringDefinition.ToString();
+
+ bool sanitiyCheckSuccess = CheckPositionExists(textLengthInBytes, bitOffset, stringDefinition.ToString());
+ if (sanitiyCheckSuccess)
+ {
+ bitReader.SetPosition(bitOffset);
+ stringDefinition.Value = ReadSingleText(bitReader, rootNode);
+
+ }
+ else
+ {
+ // mark that the text could not be read - a generic warning might be bad, because it conflates all the different texts!
+ string dummy = string.Format("Text <{0}> could not be read!", stringDefinition.ToString());
+ stringDefinition.Value = dummy;
+ }
+ }
+ }
+ }
+
+ ///
+ /// Sanity Check for Dragon age position offsets.
+ /// There can be instance where the position appears as negative (int), though i don't yet know if the uint position is also outside the array
+ ///
+ ///
+ ///
+ ///
+ /// true if the position is ok
+ private bool CheckPositionExists(int textLengthInBytes, int bitPosition, string textName)
+ {
+
+ int bytePosition = (bitPosition >> 5) * 4;
+ if ((bytePosition >= 0)
+ && (bytePosition < textLengthInBytes))
+ {
+ return true;
+ }
+
+ App.Logger.LogError(
+ "Could not read text <{0}> in resource <{1}>! The stated position is outside the data array of byte length <{4}>!",
+ textName, Name, bitPosition, bytePosition, textLengthInBytes);
+
+ return false;
+ }
+
+ ///
+ /// Returns the sorted dictionary of texts by their id, as they should be written into the resource.
+ /// Each of the dictionarys sorted by id is in turn found in a list based on where the text ids originate from,
+ /// i.e., the primary text id definition, or one of the blocks for declinated crafting adjective name-parts.
+ ///
+ /// the texts sorted by their id
+ private List> GetAllSortedTextsToWrite()
+ {
+
+ // contains the texts string sorted by their id, with position in the list by their id encountere in the resource block
+ List> allTextsToWrite = new List>();
+
+ SortedDictionary primaryTextsToWrite = new SortedDictionary();
+ allTextsToWrite.Add(primaryTextsToWrite);
+
+ foreach (var entry in GetAllPrimaryTexts())
+ {
+ primaryTextsToWrite[entry.Item1] = entry.Item2;
+ }
+
+ if (m_printVerificationTexts)
+ {
+ App.Logger.Log("..Preparing to write resource <{0}>. Added <{1}> primary texts.", Name, primaryTextsToWrite.Count);
+ }
+
+ for (int declination = 0; declination < DragonAgeDeclinatedCraftingNames.NumberOfDeclinations; declination++)
+ {
+ SortedDictionary declinatedTextsToWrite = new SortedDictionary();
+ allTextsToWrite.Add(declinatedTextsToWrite);
+
+ foreach (LocalizedStringWithId declinatedString in DragonAgeDeclinatedCraftingNames.GetAdjectivesOfDeclination(declination))
+ {
+ declinatedTextsToWrite[declinatedString.Id] = declinatedString.Value;
+ }
+ }
+
+ if (m_modifiedResource != null)
+ {
+ // shouldn't be many
+ foreach (var adjectiveEntry in m_modifiedResource.AlteredDeclinatedCraftingAdjectives)
+ {
+ uint adjectiveId = adjectiveEntry.Key;
+ List declinations = adjectiveEntry.Value;
+
+ int modiefiedDeclinationsCount = declinations.Count;
+ int currentDeclinationsCount = allTextsToWrite.Count - 1;
+
+ int iterationLimit = Math.Min(modiefiedDeclinationsCount, currentDeclinationsCount);
+ for (int i = 0; i < iterationLimit; i++)
+ {
+ allTextsToWrite[i + 1][adjectiveId] = declinations[i];
+ }
+
+ if (modiefiedDeclinationsCount > currentDeclinationsCount)
+ {
+ // I have absolutely no clue if this even works or what else might need to be changed to support additional declinations...
+ for (int i = iterationLimit; i < modiefiedDeclinationsCount; i++)
+ {
+ SortedDictionary additionalDeclinatedTextBlock = new SortedDictionary();
+ additionalDeclinatedTextBlock[adjectiveId] = declinations[i];
+
+ allTextsToWrite.Add(additionalDeclinatedTextBlock);
+ }
+ }
+ }
+ }
+
+ if (m_printVerificationTexts)
+ {
+ PrintDeclinatedAdjectivesWritingVerifications(allTextsToWrite);
+ }
+
+
+ return allTextsToWrite;
+ }
+
+ ///
+ /// Prints the verification for writing declinated adjectives.
+ ///
+ ///
+ private static void PrintDeclinatedAdjectivesWritingVerifications(List> allTextsToWrite)
+ {
+ if (allTextsToWrite.Count > 1)
+ {
+ int min = int.MaxValue;
+ int max = 0;
+
+ for (int groupId = 1; groupId < allTextsToWrite.Count; groupId++)
+ {
+ var group = allTextsToWrite[groupId];
+ int groupSize = group.Count;
+ min = min > groupSize ? groupSize : min;
+ max = max < groupSize ? groupSize : max;
+ }
+
+ string groupSizeText;
+ if (min == max)
+ {
+ groupSizeText = $"of {max} number of text ids";
+ }
+ else
+ {
+ groupSizeText = $"of beween {min} and {max} number of texts";
+ }
+
+ App.Logger.Log("... Added <{0}> groups of declinated crafting adjectives {1}.", allTextsToWrite.Count - 1, groupSizeText);
+ }
+ }
+
+ ///
+ /// Returns the encoding originally used, if all characters are included. Otherwise recalculates a new encoding.
+ ///
+ /// The enumeration of all texts and their id
+ /// The root huffman node for the encoding
+ private HuffmanNode GetEncodingRootNode(List> allSortedTexts)
+ {
+
+ if (m_modifiedResource == null)
+ {
+ return m_encodingRootNode;
+ }
+
+ // compare added texts chars to allowed chars, if new ones, recalculate encoding
+ IEnumerable