using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; using ScriptTool; using System.IO; using Newtonsoft.Json; namespace ScriptToolGui { public partial class MainForm : Form { // Config Config config; // Static/const members static Compiler m12Compiler = new Compiler(M12ControlCode.Codes, (rom, address) => rom[address + 1] == 0xFF); static Compiler ebCompiler = new Compiler(EbControlCode.Codes, (rom, address) => rom[address] < 0x20); static readonly Game[] validGames; static IDictionary ebCharLookup; // Lookups Dictionary textboxLookup; Dictionary> stringsLookup; // Saving changes object changeLock = new object(); bool changesMade = false; // Strings List m12Strings; List m12StringsEnglish; List ebStrings; // Index mappings IndexMapping itemMapping = new IndexMapping(); // Matched reference pairs MatchedGroupCollection tptGroups = new MatchedGroupCollection("TPT", Game.Eb, Game.M12, Game.M12English); MatchedGroupCollection battleActionGroups = new MatchedGroupCollection("Battle actions", Game.Eb, Game.M12, Game.M12English); MatchedGroupCollection itemHelpGroups = new MatchedGroupCollection("Item help", Game.Eb, Game.M12, Game.M12English); MatchedGroupCollection psiHelpGroups = new MatchedGroupCollection("PSI help", Game.Eb, Game.M12, Game.M12English); MatchedGroupCollection rawM12Groups = new MatchedGroupCollection("Raw M12 strings", Game.M12, Game.M12English); MatchedGroupCollection rawEbGroups = new MatchedGroupCollection("Raw EB strings", Game.Eb); List matchedCollections = new List(); // Navigation stack IDictionary currentIndex; NavigationEntry previousNavigationState = null; Stack navigationStack = new Stack(); MatchedGroupCollection currentCollection = null; // Misc bool badStart = false; static MainForm() { validGames = new Game[] { Game.Eb, Game.M12, Game.M12English }; ebCharLookup = JsonConvert.DeserializeObject>(File.ReadAllText("eb-char-lookup.json")); } string ReadEbString(byte[] rom, int address, int length) { var sb = new StringBuilder(); for(int i=0;i this.Close(); } } private void PopulateCollectionSelector() { collectionSelector.Items.Clear(); collectionSelector.Items.AddRange(matchedCollections.ToArray()); } private void PopulateGroupSelector(MatchedGroupCollection collection) { groupSelector.Items.Clear(); if (collection != null) { groupSelector.Items.AddRange(collection.Groups.ToArray()); } } private void InitLookups() { textboxLookup = new Dictionary { { Game.Eb, ebString }, { Game.M12, m12String }, { Game.M12English, m12StringEnglish } }; stringsLookup = new Dictionary> { { Game.Eb, ebStrings }, { Game.M12, m12Strings }, { Game.M12English, m12StringsEnglish } }; currentIndex = new Dictionary { { Game.Eb, -1 }, { Game.M12, -1 }, { Game.M12English,-1 } }; } private void ImportAllStringRefs() { // TPT var m12PrimaryTptRefs = ImportStringRefs("m12-tpt-primary.json"); var ebPrimaryTptRefs = ImportStringRefs("eb-tpt-primary.json"); var m12SecondaryTptRefs = ImportStringRefs("m12-tpt-secondary.json"); var ebSecondaryTptRefs = ImportStringRefs("eb-tpt-secondary.json"); tptGroups.Groups.AddRange(MatchRefs(ebPrimaryTptRefs, m12PrimaryTptRefs)); tptGroups.Groups.AddRange(MatchRefs(ebSecondaryTptRefs, m12SecondaryTptRefs)); tptGroups.SortGroups(); // Battle actions var m12BattleActionRefs = ImportStringRefs("m12-battle-actions.json"); var ebBattleActionRefs = ImportStringRefs("eb-battle-actions.json"); battleActionGroups.Groups.AddRange(MatchRefs(ebBattleActionRefs, m12BattleActionRefs)); battleActionGroups.SortGroups(); // Item help itemMapping = JsonConvert.DeserializeObject(File.ReadAllText("item-map.json")); var m12ItemHelpRefs = ImportStringRefs("m12-item-help.json"); var ebItemHelpRefs = ImportStringRefs("eb-item-help.json"); var itemHelpMappingGroups = itemMapping.Select(p => new MatchedGroup( ebItemHelpRefs.First(e => e.Index == p.First), m12ItemHelpRefs.First(m => m.Index == p.Second))) .OrderBy(g => g.Refs[Game.Eb].Index) .ToArray(); itemHelpGroups.Groups.AddRange(itemHelpMappingGroups); var itemNames = JsonConvert.DeserializeObject(File.ReadAllText(@"..\..\..\..\working\m12-itemnames.json")); var ebItems = itemMapping.Select(p => new { Index = p.First, Name = itemNames.StringRefs[p.Second].New }) .OrderBy(o => o.Index) .Select(m => "[" + (m.Index).ToString("X2") + "] " + m.Name.Substring(0, m.Name.Length - 7)) .ToArray(); File.WriteAllLines(@"D:\eb-itemnames.txt", ebItems); // PSI help var m12PsiHelpRefs = ImportStringRefs("m12-psi-help.json"); var ebPsiHelpRefs = ImportStringRefs("eb-psi-help.json"); var psiHelpMappingGroups = ebPsiHelpRefs.Select(e => new MatchedGroup(e, m12PsiHelpRefs.First(m => m.Index == e.Index - 1))) .ToArray(); psiHelpGroups.Groups.AddRange(psiHelpMappingGroups); psiHelpGroups.SortGroups(); // Raw M12 var labels = Enumerable.Range(0, 5864); foreach (int i in labels) { rawM12Groups.Groups.Add(new MatchedGroup(i, "L" + i.ToString())); } // Raw EB labels = Enumerable.Range(0, 5357); foreach (int i in labels) { rawEbGroups.Groups.Add(new MatchedGroup(Game.Eb, i, "L" + i.ToString())); } matchedCollections.Add(tptGroups); matchedCollections.Add(battleActionGroups); matchedCollections.Add(itemHelpGroups); matchedCollections.Add(psiHelpGroups); matchedCollections.Add(rawM12Groups); matchedCollections.Add(rawEbGroups); } private MatchedGroup[] MatchRefs(MainStringRef[] ebRefs, MainStringRef[] m12Refs) { return ebRefs.Join(m12Refs, e => e.Index, m => m.Index, (e, m) => new { e, m }) .Select(p => new MatchedGroup(p.e, p.m)) .ToArray(); } private MainStringRef[] ImportStringRefs(string fileName) { string jsonString = File.ReadAllText(Path.Combine(config.WorkingFolder, fileName)); return JsonConvert.DeserializeObject(jsonString); } private void ImportAllStrings() { string m12FileName = Path.Combine(config.WorkingFolder, "m12-strings.txt"); string m12EnglishFileName = Path.Combine(config.WorkingFolder, "m12-strings-english.txt"); string ebFileName = Path.Combine(config.WorkingFolder, "eb-strings.txt"); m12Strings = ImportStrings(m12FileName); m12StringsEnglish = ImportStrings(m12EnglishFileName); ebStrings = ImportStrings(ebFileName); } private List ImportStrings(string fileName) { return new List(File.ReadAllLines(fileName).Where(l => !l.Equals(""))); } private Game GetCurrentGame() { if (ebSelector.Checked) return Game.Eb; else if (m12Selector.Checked) return Game.M12; return Game.None; } private void PopulateCodeList() { codeList.Items.Clear(); ISet codes = null; if (ebSelector.Checked) ebCompiler.ScanString(ebString.Text, ebCharLookup, true, out codes); else if (m12Selector.Checked) m12Compiler.ScanString(m12String.Text, ebCharLookup, true, out codes); var sorted = codes.Distinct().OrderBy(c => c).ToArray(); codeList.Items.AddRange(sorted.ToArray()); } private void PopulateReferenceList() { referenceList.Items.Clear(); IList references = null; if (ebSelector.Checked) ebCompiler.ScanString(ebString.Text, ebCharLookup, true, out references); else if (m12Selector.Checked) m12Compiler.ScanString(m12String.Text, ebCharLookup, true, out references); references = references.Distinct().OrderBy(r => r).ToList(); referenceList.Items.AddRange(references.ToArray()); } private string GetString(Game game, MatchedGroup group) { int index; return GetString(game, group, out index); } private string GetString(Game game, MatchedGroup group, out int index) { if (!group.Refs.ContainsKey(game)) { index = -1; return null; } return GetString(game, group.Refs[game].Label, out index); } private string GetString(Game game, string label, out int index) { string labelDef = "^" + label + "^"; var str = stringsLookup[game].Select((l, i) => new { Index = i, Line = l }) .FirstOrDefault(a => a.Line.Contains(labelDef)); if (str == null) index = -1; else index = str.Index; if (str == null) return null; else return str.Line; } private void NavigateTo(MatchedGroup group, MatchedGroupCollection collection) { if (group == null) { ebString.Text = ""; m12String.Text = ""; m12StringEnglish.Text = ""; } else { int index; string eb = GetString(Game.Eb, group, out index); currentIndex[Game.Eb] = index; string m12 = GetString(Game.M12, group, out index); currentIndex[Game.M12] = index; string m12English = GetString(Game.M12English, group, out index); currentIndex[Game.M12English] = index; ebString.Text = eb; m12String.Text = m12; m12StringEnglish.Text = m12English; previousNavigationState = new MatchedGroupNavigationEntry(group, collection); if (m12 != null && m12 == m12Compiler.StripText(m12)) { m12String.BackColor = Color.Orange; } else { m12String.BackColor = Color.White; } } PopulateCodeList(); PopulateReferenceList(); previewButton_Click(null, null); } private void SelectGroup(MatchedGroup group, MatchedGroupCollection collection) { if (collection != null) { if ((MatchedGroupCollection)collectionSelector.SelectedItem != collection) { collectionSelector.SelectedItem = collection; PopulateGroupSelector(collection); } if (group != null) { groupSelector.SelectedItem = group; } else { groupSelector.SelectedIndex = -1; } } else { collectionSelector.SelectedIndex = -1; groupSelector.SelectedIndex = -1; } currentCollection = collection; } private void FindGroup(Game game, string label, out MatchedGroup group, out MatchedGroupCollection collection) { // Attempt to find the label string labelDef = "^" + label + "^"; string str = stringsLookup[game].First(s => s.Contains(labelDef)); foreach (var coll in matchedCollections.Where(c => c.Games.Contains(game))) { var match = coll.Groups.FirstOrDefault(g => str.Contains("^" + g.Refs[game].Label + "^")); if (match != null) { group = match; collection = coll; return; } } group = null; collection = null; } private void NavigateTo(Game game, string label, out MatchedGroup group, out MatchedGroupCollection collection) { foreach (var eachGame in validGames) { currentIndex[eachGame] = -1; textboxLookup[eachGame].Text = ""; } string labelDef = "^" + label + "^"; int index; textboxLookup[game].Text = GetString(game, label, out index); currentIndex[game] = index; if (game == Game.M12 || game == Game.M12English) { if (game == Game.M12) { textboxLookup[Game.M12English].Text = GetString(Game.M12English, label, out index); currentIndex[Game.M12English] = index; } else if (game == Game.M12English) { textboxLookup[Game.M12].Text = GetString(Game.M12, label, out index); currentIndex[Game.M12] = index; } string m12 = textboxLookup[Game.M12].Text; if (m12 != null && m12 == m12Compiler.StripText(m12)) { m12String.BackColor = Color.Orange; } else { m12String.BackColor = Color.White; } } else { m12String.BackColor = Color.White; } previousNavigationState = new ReferenceNavigationEntry(game, label); FindGroup(game, label, out group, out collection); // Check if any other games have this matched ref if (group != null) { foreach (var otherGame in group.Refs.Where(kv => kv.Key != game)) { labelDef = "^" + otherGame.Value.Label + "^"; textboxLookup[otherGame.Key].Text = GetString(otherGame.Key, otherGame.Value.Label, out index); currentIndex[game] = index; } } PopulateCodeList(); PopulateReferenceList(); previewButton_Click(null, null); } private void PushPreviousNavigationState() { if (previousNavigationState == null) return; navigationStack.Push(previousNavigationState); } private async void SaveCurrentState(bool insertEndcode) { string endcodeInsertion = null; lock (changeLock) { foreach (var game in validGames) { if (currentIndex[game] >= 0) { string oldString = stringsLookup[game][currentIndex[game]]; string newString = textboxLookup[game].Text.Replace(Environment.NewLine, ""); if (game == Game.M12English) { if (oldString != newString) { changesMade = true; } if (insertEndcode) { // Special case -- for M12 English, if the line is modified, // check for an end code and insert if it's missing var lastCode = m12Compiler.GetLastControlCode(newString); if (!IsJustALabel(newString) && (lastCode == null || (lastCode != null && !lastCode.IsEnd))) { newString += "[00 FF]"; endcodeInsertion = "Inserted missing [00 FF]"; } } } stringsLookup[game][currentIndex[game]] = newString; if (!textboxLookup[game].Text.Equals(newString)) { // Update the text box as well textboxLookup[game].Text = newString; } } } } if (endcodeInsertion != null) { await SetMessageLabel(endcodeInsertion); } } public static bool IsJustALabel(string str) { return (str.Length > 2) && (str[0] == '^') && (str[str.Length - 1] == '^') && !str.Skip(1).Take(str.Length - 2).Contains('^'); } private void WriteChanges(bool insertEndcode) { SaveCurrentState(insertEndcode); lock (changeLock) { //if (changesMade) { using (StreamWriter sw = File.CreateText(Path.Combine(config.WorkingFolder, "m12-strings-english.txt"))) { foreach (string line in m12StringsEnglish) { sw.WriteLine(line); } } UpdateStatus(String.Format("Last saved: {0:G}", DateTime.Now)); changesMade = false; } } } private static IList ExtractLabels(string str) { if (str == null) return null; var labels = new List(); for (int i = 0; i < str.Length; ) { // Find the first caret if (str[i] == '^') { // Find the next caret bool foundNext = false; for (int j = i + 1; j < str.Length; j++) { if (str[j] == '^') { // Get the label string label = str.Substring(i + 1, j - i - 1); labels.Add(label); i = j + 1; foundNext = true; break; } } if (!foundNext) { throw new Exception("Found opening caret with no closing caret: " + str); } } else { i++; } } return labels; } private void UpdateStatus(string text) { if (statusBar.InvokeRequired) { statusBar.Invoke(new Action(UpdateStatus), text); } else { writeLabel.Text = text; } } private void groupSelector_SelectionChangeCommitted(object sender, EventArgs e) { SaveCurrentState(true); if (groupSelector.SelectedIndex == -1) { NavigateTo(null, null); } else { PushPreviousNavigationState(); var currentGroup = (MatchedGroup)groupSelector.SelectedItem; var currentCollection = (MatchedGroupCollection)collectionSelector.SelectedItem; NavigateTo(currentGroup, currentCollection); } } private void gameSelector_CheckedChanged(object sender, EventArgs e) { PopulateCodeList(); PopulateReferenceList(); } private void referenceList_MouseDoubleClick(object sender, MouseEventArgs e) { int match = referenceList.IndexFromPoint(e.Location); if (match != ListBox.NoMatches) { Game game = GetCurrentGame(); string label = (string)referenceList.SelectedItem; // Only navigate if we're not already at the target label if (!stringsLookup[game].Contains("^" + label + "^")) { SaveCurrentState(true); PushPreviousNavigationState(); MatchedGroup group; MatchedGroupCollection collection; NavigateTo(game, label, out group, out collection); SelectGroup(group, collection); } } } private void backButton_Click(object sender, EventArgs e) { if (navigationStack.Count < 1) return; SaveCurrentState(true); var nav = navigationStack.Pop(); if (nav.Type == NavigationType.MatchedGroup) { var matchedEntry = (MatchedGroupNavigationEntry)nav; NavigateTo(matchedEntry.Group, matchedEntry.Collection); SelectGroup(matchedEntry.Group, matchedEntry.Collection); } else if (nav.Type == NavigationType.Reference) { var referenceEntry = (ReferenceNavigationEntry)nav; MatchedGroup group; MatchedGroupCollection collection; NavigateTo(referenceEntry.Game, referenceEntry.Label, out group, out collection); SelectGroup(group, collection); } } private void saveMenu_Click(object sender, EventArgs e) { WriteChanges(true); } private void writeTimer_Tick(object sender, EventArgs e) { WriteChanges(false); } private void MainForm_FormClosing(object sender, FormClosingEventArgs e) { if (badStart) return; writeTimer.Enabled = false; WriteChanges(true); } private void copyCodesButton_Click(object sender, EventArgs e) { m12StringEnglish.Text = m12Compiler.StripText(m12String.Text); groupSelector.Focus(); } private void collectionSelector_SelectionChangeCommitted(object sender, EventArgs e) { if (collectionSelector.SelectedIndex == -1) { groupSelector.Items.Clear(); previewer.DisplayedString = null; } else { var collection = (MatchedGroupCollection)collectionSelector.SelectedItem; // Set the previewer width before we do navigation stuff if (collection == psiHelpGroups) { previewer.MaxWidth = 224; } else if (collection == battleActionGroups) { previewer.MaxWidth = 176; } else { previewer.MaxWidth = 144; } // Take no action if we haven't actually changed the collection // (otherwise, the group selector would jump to 0, probably unwanted) if (collection == currentCollection) return; currentCollection = collection; PopulateGroupSelector(collection); groupSelector.SelectedIndex = 0; groupSelector_SelectionChangeCommitted(null, null); } } private void previewButton_Click(object sender, EventArgs e) { previewer.DisplayedString = m12StringEnglish.Text; } private void m12StringEnglish_MouseClick(object sender, MouseEventArgs e) { if (ModifierKeys == Keys.Control) { // Insert a line break int currentSelectionStart = m12StringEnglish.SelectionStart; m12StringEnglish.Text = m12StringEnglish.Text.Insert(m12StringEnglish.SelectionStart, "[01 FF]"); m12StringEnglish.SelectionStart = currentSelectionStart + 7; } } private async Task SetMessageLabel(string message) { var messageLabel = new ToolStripStatusLabel(message); messageLabel.Font = new Font(messageLabel.Font, FontStyle.Bold); messageLabel.BackColor = SystemColors.Highlight; statusBar.Items.Add(messageLabel); await Task.Delay(3000); statusBar.Items.Remove(messageLabel); } private void resolveDuplicateLabelsMenu_Click(object sender, EventArgs e) { SaveCurrentState(false); try { // Enumerate all labels var labels = m12StringsEnglish.Select((s, i) => new { Labels = ExtractLabels(s), Index = i }); // Find duplicates var flattened = labels.SelectMany(l => l.Labels.Select(b => new { Label = b, Index = l.Index })); var duplicates = flattened.GroupBy(f => f.Label) .Where(g => g.Count() > 1) .Select(g => new { Label = g.Key, Indices = g.Select(r => new { Index = r.Index, Trivial = IsJustALabel(m12StringsEnglish[r.Index]) }).ToList() }) .ToList(); // Find the duplicates that can be resolved var canBeResolved = duplicates.Where(d => d.Indices.Where(i => !i.Trivial).Count() <= 1).ToList(); // Find the duplicates that can't be resolved var cantBeResolved = duplicates.Where(d => d.Indices.Where(i => !i.Trivial).Count() > 1).ToList(); if (duplicates.Count == 0) { MessageBox.Show("No duplicates found.", "Duplicates", MessageBoxButtons.OK, MessageBoxIcon.Information); } else { string message = "Duplicates were found."; if (canBeResolved.Count > 0) { message += Environment.NewLine + Environment.NewLine + "The following duplicates may be resolved automatically:" + Environment.NewLine + String.Join(", ", canBeResolved.Select(d => d.Label).ToArray()); } if (cantBeResolved.Count > 0) { message += Environment.NewLine + Environment.NewLine + "The following duplicates cannot be resolved manually:" + Environment.NewLine + String.Join(", ", cantBeResolved.Select(d => d.Label).ToArray()); } if (canBeResolved.Count > 0) { message += Environment.NewLine + Environment.NewLine + "Would you like to fix the resolvable duplicates now?"; var dialogResult = MessageBox.Show(message, "Duplicates", MessageBoxButtons.YesNo, MessageBoxIcon.Warning); if (dialogResult == DialogResult.Yes) { // The trivial duplicates may simply be removed from the strings collection var toBeRemoved = canBeResolved.SelectMany(d => d.Indices) .Where(i => i.Trivial) .Select(i => i.Index) .Distinct() .OrderByDescending(i => i); // Make sure we aren't currently on a string that's about to be removed (shouldn't ever actually happen...) if (toBeRemoved.Any(i => i == currentIndex[Game.M12English])) { MessageBox.Show("Could not remove duplicates because one of them is currently selected! Navigate away first.", "Could not resolve", MessageBoxButtons.OK, MessageBoxIcon.Error); return; } foreach (int i in toBeRemoved) { // Remove the string m12StringsEnglish.RemoveAt(i); // Update currentIndex if (currentIndex[Game.M12English] > i) { currentIndex[Game.M12English]--; } } MessageBox.Show("Duplicates removed!", "Finished", MessageBoxButtons.OK, MessageBoxIcon.Information); } } else { MessageBox.Show(message, "Duplicates", MessageBoxButtons.OK, MessageBoxIcon.Warning); } } } catch (Exception ex) { MessageBox.Show("Error extracting labels. " + ex.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); return; } } private void checkCompletionMenu_Click(object sender, EventArgs e) { int completedCount = m12StringsEnglish.Count(s => !IsJustALabel(s)); int total = m12StringsEnglish.Count; MessageBox.Show(String.Format("Completed: {0}/{1} ({2:F2}%)", completedCount, total, (double)completedCount * 100d / (double)total)); } private void prevButton_Click(object sender, EventArgs e) { if (groupSelector.SelectedIndex < 1) { return; } groupSelector.SelectedIndex--; groupSelector_SelectionChangeCommitted(null, null); } private void nextButton_Click(object sender, EventArgs e) { if (groupSelector.SelectedIndex == -1 || groupSelector.SelectedIndex == groupSelector.Items.Count - 1) { return; } groupSelector.SelectedIndex++; groupSelector_SelectionChangeCommitted(null, null); } } enum Game { None, Eb, M2, M12, M12English } }