using HavenSoft.HexManiac.Core.Models; using HavenSoft.HexManiac.Core.Models.Code; using HavenSoft.HexManiac.Core.Models.Map; using HavenSoft.HexManiac.Core.Models.Runs; using HavenSoft.HexManiac.Core.Models.Runs.Factory; using HavenSoft.HexManiac.Core.Models.Runs.Sprites; using HavenSoft.HexManiac.Core.ViewModels.DataFormats; using HavenSoft.HexManiac.Core.ViewModels.Images; using HavenSoft.HexManiac.Core.ViewModels.Tools; using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Text; using System.Threading.Tasks; // example for making a bug trainer: templates.CreateTrainer(objectEvent, history.CurrentChange, 20 /* bug catcher */, 30, 9, 6 /*bug*/, true); namespace HavenSoft.HexManiac.Core.ViewModels.Map { public interface IDataInvestigator { int FindNextUnusedFlag(); int FindNextUnusedVariable(); } public class EventTemplate : ViewModelCore, IDataInvestigator { private readonly Random rnd = new(); private readonly IDataModel model; private readonly ScriptParser parser; private readonly Task initializationWorkload; private ISet usedFlags, usedTrainerFlags, usedVariables; private IReadOnlyDictionary trainerPreferences; private IReadOnlyDictionary minLevel; private ISet UsedFlags { get { initializationWorkload.Wait(); return usedFlags; } } private ISet UsedTrainerFlags { get { initializationWorkload.Wait(); return usedTrainerFlags; } } private ISet UsedVariables { get { initializationWorkload.Wait(); return usedVariables; } } public void UseTrainerFlag(int flag) => UsedTrainerFlags.Add(flag); public bool IsTrainerFlagInUse(int flag) => UsedTrainerFlags.Contains(flag); private IReadOnlyDictionary TrainerPreferences { get { if (trainerPreferences == null) trainerPreferences = Flags.GetTrainerPreference(model, parser); return trainerPreferences; } } private IReadOnlyDictionary MinLevel { get { if (minLevel == null) minLevel = Flags.GetMinimumLevelForPokemon(model); return minLevel; } } public IPixelViewModel ObjectTemplateImage { get; private set; } public IReadOnlyList OverworldGraphics { get; private set; } public EventTemplate(IWorkDispatcher dispatcher, IDataModel model, ScriptParser parser, IReadOnlyList owGraphics) { (this.model, this.parser) = (model, parser); RefreshLists(owGraphics); if (model.IsFRLG()) UseNationalDex = true; HMObjectOptions.Add("Cut Tree"); HMObjectOptions.Add("Smash Rock"); HMObjectOptions.Add("Strength Boulder"); TrainerOptions.Bind(nameof(TrainerOptions.SelectedIndex), (options, args) => { var targetSprite = model.GetTableModel(HardcodeTablesModel.TrainerTableName)[TrainerOptions.SelectedIndex].GetValue("sprite"); foreach (var key in TrainerPreferences.Keys) { if (TrainerPreferences[key].Sprite != targetSprite) continue; TrainerGraphics = key; break; } }); initializationWorkload = dispatcher.RunBackgroundWork(() => { usedFlags = Flags.GetUsedItemFlags(model, parser); usedTrainerFlags = Flags.GetUsedTrainerFlags(model, parser); usedVariables = Flags.GetUsedVariables(model, parser); }); } public void RefreshLists(IReadOnlyList owGraphics) { OverworldGraphics = owGraphics; AvailableTemplateTypes.Clear(); AvailableTemplateTypes.Add(TemplateType.None); AvailableTemplateTypes.Add(TemplateType.Npc); AvailableTemplateTypes.Add(TemplateType.Item); AvailableTemplateTypes.Add(TemplateType.Trainer); AvailableTemplateTypes.Add(TemplateType.Mart); AvailableTemplateTypes.Add(TemplateType.Trade); if (model.IsFRLG() || model.IsEmerald()) AvailableTemplateTypes.Add(TemplateType.Tutor); // Ruby/Sapphire don't have tutors AvailableTemplateTypes.Add(TemplateType.Legendary); AvailableTemplateTypes.Add(TemplateType.HMObject); GraphicsOptions.Clear(); for (int i = 0; i < owGraphics.Count; i++) GraphicsOptions.Add(VisualComboOption.CreateFromSprite(i.ToString(), owGraphics[i].PixelData, owGraphics[i].PixelWidth, i, 2, true)); TypeOptions.Clear(); var types = model.GetTableModel(HardcodeTablesModel.TypesTableName); if (types != null) { foreach (var type in types) { TypeOptions.Add(type.GetStringValue("name")); } } ItemOptions.Clear(); var items = model.GetTableModel(HardcodeTablesModel.ItemsTableName); if (items != null) { foreach (var item in items) { ItemOptions.Add(item.GetStringValue("name")); } } var trainerOptions = new List(); var trainers = model.GetTableModel(HardcodeTablesModel.TrainerTableName); var trainerClasses = model.GetTableModel(HardcodeTablesModel.TrainerClassNamesTable); if (trainers != null && trainerClasses != null) { var options = model.GetOptions(HardcodeTablesModel.TrainerClassNamesTable); for (int i = 0; i < trainers.Count; i++) { trainerOptions.Add(ObjectEventViewModel.CreateOption(options, i, trainers[i].GetValue(1), trainers[i].GetStringValue("name"))); } } TrainerOptions.Update(trainerOptions, TrainerOptions.SelectedIndex); UpdateObjectTemplateImage(); } private TemplateType selectedTemplate; public TemplateType SelectedTemplate { get => selectedTemplate; set { SetEnum(ref selectedTemplate, value, UpdateObjectTemplateImage); if (selectedTemplate == TemplateType.HMObject) UpdateSpriteFromHMObject(); } } public ObservableCollection AvailableTemplateTypes { get; } = new(); public int FindNextUnusedFlag() { var flag = 0x21; while (UsedFlags.Contains(flag)) flag++; UsedFlags.Add(flag); return flag; } public int FindNextUnusedVariable() { var variable = 0x4034; while (UsedVariables.Contains(variable)) variable++; UsedVariables.Add(variable); return variable; } public void ApplyTemplate(ObjectEventViewModel objectEventModel, ModelDelta token) { if (selectedTemplate == TemplateType.Trainer) CreateTrainer(objectEventModel, token); if (selectedTemplate == TemplateType.Npc) CreateNPC(objectEventModel, token); if (selectedTemplate == TemplateType.Item) CreateItem(objectEventModel, token); if (selectedTemplate == TemplateType.Mart) CreateMart(objectEventModel, token); if (selectedTemplate == TemplateType.Tutor) CreateTutor(objectEventModel, token); if (selectedTemplate == TemplateType.Trade) CreateTrade(objectEventModel, token); if (selectedTemplate == TemplateType.Legendary) CreateLegendary(objectEventModel, token); if (selectedTemplate == TemplateType.HMObject) CreateHMObject(objectEventModel, token); } #region Trainer public ObservableCollection GraphicsOptions { get; } = new(); public ObservableCollection TypeOptions { get; } = new(); private bool useExistingTrainer; public bool UseExistingTrainer { get => useExistingTrainer; set => Set(ref useExistingTrainer, value); } public FilteringComboOptions TrainerOptions { get; } = new(); private int trainerGraphics, maxPokedex = 25, maxLevel = 9, preferredType = 6; public int TrainerGraphics { get => trainerGraphics; set { Set(ref trainerGraphics, value, old => { if (!TrainerPreferences.TryGetValue(trainerGraphics, out var pref)) pref = new(0, 0, 0); var spriteAddress = model.GetTableModel(HardcodeTablesModel.TrainerSpritesName)[pref.Sprite].GetAddress("sprite"); var spriteRun = model.GetNextRun(spriteAddress) as ISpriteRun; TrainerSprite = ReadonlyPixelViewModel.Create(model, spriteRun, true); NotifyPropertyChanged(nameof(TrainerSprite)); UpdateObjectTemplateImage(); }); } } public int MaxPokedex { get => maxPokedex; set => Set(ref maxPokedex, value); } public int MaxLevel { get => maxLevel; set => Set(ref maxLevel, value); } public int PreferredType { get => preferredType; set => Set(ref preferredType, value); } private bool useNationalDex; public bool UseNationalDex { get => useNationalDex; set => Set(ref useNationalDex, value); } public IPixelViewModel TrainerSprite { get; private set; } // TODO use all-caps name or mixed-caps name depending on other trainers in the table // TODO use reference file to get names and before/win/after text public void CreateTrainer(ObjectEventViewModel objectEventModel, ModelDelta token) { const int ChosenTypeOddsMultiplier = 100; var trainers = model.GetTableModel(HardcodeTablesModel.TrainerTableName, () => token); var trainerFlag = 1; if (UseExistingTrainer) { trainerFlag = TrainerOptions.SelectedIndex; UsedTrainerFlags.Add(trainerFlag); } else { // part 1: the team var availablePokemon = new List(); var dexName = HardcodeTablesModel.RegionalDexTableName; if (useNationalDex) dexName = HardcodeTablesModel.NationalDexTableName; var pokedex = model.GetTableModel(dexName, () => token); var pokestats = model.GetTableModel(HardcodeTablesModel.PokemonStatsTable, () => token); for (int i = 1; i < pokedex.Count; i++) { if (pokedex[i - 1].GetValue(0) > maxPokedex) continue; if (MinLevel.TryGetValue(i, out var level) && level > maxLevel) continue; availablePokemon.Add(i); if (pokestats[i].GetValue("type1") == preferredType || pokestats[i].GetValue("type2") == preferredType) { for (int j = 0; j < ChosenTypeOddsMultiplier; j++) availablePokemon.Add(i); } } var teamSize = rnd.Next(3) + 1; var teamStart = model.FindFreeSpace(model.FreeSpaceStart, 8 * teamSize); for (int i = 0; i < teamSize; i++) { // ivSpread: level: mon: padding: var pokemon = availablePokemon[rnd.Next(availablePokemon.Count)]; var level = maxLevel; while (level > maxLevel - 5 && rnd.Next(2) == 1) level--; model.WriteMultiByteValue(teamStart + i * 8 + 0, 2, token, 0); model.WriteMultiByteValue(teamStart + i * 8 + 2, 2, token, level); model.WriteMultiByteValue(teamStart + i * 8 + 4, 2, token, pokemon); model.WriteMultiByteValue(teamStart + i * 8 + 6, 2, token, 0); } // part 2: the trainer while (UsedTrainerFlags.Contains(trainerFlag)) trainerFlag++; usedTrainerFlags.Add(trainerFlag); var trainer = trainers[trainerFlag]; // structType. class. introMusicAndGender. sprite. name""12 item1: item2: item3: item4: doubleBattle:: ai:: pokemonCount:: pokemon<> trainer.SetValue("structType", 0); trainer.SetStringValue("name", "Francis"); trainer.SetValue("item1", 0); trainer.SetValue("item2", 0); trainer.SetValue("item3", 0); trainer.SetValue("item4", 0); trainer.SetValue("doubleBattle", 0); trainer.SetValue("ai", 0); trainer.SetValue("pokemonCount", teamSize); trainer.SetAddress("pokemon", teamStart); if (!TrainerPreferences.TryGetValue(trainerGraphics, out var pref)) pref = new(0, 0, 0); trainer.SetValue("class", pref.TrainerClass); trainer.SetValue("introMusicAndGender", pref.MusicAndGender); trainer.SetValue("sprite", pref.Sprite); } // part 3: the script /* trainerbattle 00 102 0 loadpointer 0 callstd 6 end */ // 2 6 10 16 // 5C 00 trainerFlag: 00 00 0F 00 09 06 02 var before = WriteText(token, "Let's battle!"); var win = WriteText(token, "You Win!"); var after = WriteText(token, "Post-battle chat!"); var scriptStart = model.FindFreeSpace(model.FreeSpaceStart, 24); token.ChangeData(model, scriptStart, "5C 00 00 00 00 00 00 00 00 00 00 00 00 00 0F 00 00 00 00 00 09 06 02 00".ToByteArray()); model.WriteMultiByteValue(scriptStart + 2, 2, token, trainerFlag); model.WritePointer(token, scriptStart + 6, before); model.WritePointer(token, scriptStart + 10, win); model.WritePointer(token, scriptStart + 16, after); model.ObserveRunWritten(token, new PointerRun(scriptStart + 6)); model.ObserveRunWritten(token, new PointerRun(scriptStart + 10)); model.ObserveRunWritten(token, new PointerRun(scriptStart + 16)); var factory = new PCSRunContentStrategy(); factory.TryAddFormatAtDestination(model, token, scriptStart + 6, before, default, default, default); factory.TryAddFormatAtDestination(model, token, scriptStart + 10, win, default, default, default); factory.TryAddFormatAtDestination(model, token, scriptStart + 16, after, default, default, default); // part 4: the event objectEventModel.Graphics = trainerGraphics; objectEventModel.Elevation = FindPreferredTrainerElevation(model, trainerGraphics); objectEventModel.MoveType = new[] { 7, 8, 9, 10 }[rnd.Next(4)]; objectEventModel.RangeX = objectEventModel.RangeY = 1; objectEventModel.TrainerType = 1; objectEventModel.TrainerRangeOrBerryID = 5; objectEventModel.ScriptAddress = scriptStart; objectEventModel.Flag = 0; objectEventModel.RefreshTrainerOptions(); model.ObserveRunWritten(token, new XSERun(scriptStart, SortedSpan.One(objectEventModel.Start + 16))); } private int FindPreferredTrainerElevation(IDataModel model, int graphics) { var histogram = new Dictionary(); var banks = AllMapsModel.Create(model, null); if (banks == null) return 3; foreach (var bank in banks) { foreach (var map in bank) { if (map == null) continue; foreach (var obj in map.Events.Objects) { if (obj.Graphics != graphics) continue; if (!histogram.ContainsKey(obj.Elevation)) histogram[obj.Elevation] = 0; histogram[obj.Elevation]++; } } } return histogram.MostCommonKey(); } public TrainerEventContent GetTrainerContent(IEventViewModel eventModel) => GetTrainerContent(model, eventModel); public static TrainerEventContent GetTrainerContent(IDataModel model, IEventViewModel eventModel) { if (eventModel is not ObjectEventViewModel objectModel) return null; var address = objectModel.ScriptAddress; if (address < 0) return null; // 5C 00 trainerFlag: 00 00 0F 00 09 06 02 var expectedValues = new Dictionary { { 0, 0x5C }, { 1, 0x00 }, { 4, 0x00 }, { 5, 0x00 }, { 14, 0x0F }, { 15, 0x00 }, { 20, 0x09 }, { 21, 0x06 }, { 22, 0x02 }, }; if (address >= model.Count - expectedValues.Count) return null; foreach (var (k, v) in expectedValues) { if (model[address + k] != v) return null; } var trainerID = model.ReadMultiByteValue(address + 2, 2); if (trainerID < 0) return null; var trainers = model.GetTableModel(HardcodeTablesModel.TrainerTableName, null); if (trainerID >= trainers.Count) return null; var beforePointer = address + 6; var winPointer = address + 10; var afterPointer = address + 16; var trainerClassAddress = trainers[trainerID].Start + 1; var trainerNameAddress = trainers[trainerID].Start + 4; var teamPointer = trainers[trainerID].Start + 36; return new TrainerEventContent(beforePointer, winPointer, afterPointer, trainerClassAddress, trainerID, address + 2, trainerNameAddress, teamPointer); } #endregion #region NPC public void CreateNPC(ObjectEventViewModel eventModel, ModelDelta token) { // loadpointer 0 ; callstd 2; end; text="I'm an NPC!" // 0F 00 09 02 02 "I'm an NPC!" var scriptStart = model.FindFreeSpace(model.FreeSpaceStart, 24); token.ChangeData(model, scriptStart, "0F 00 00 00 00 00 09 02 02 C3 B4 E1 00 D5 E2 00 C8 CA BD AB FF".ToByteArray()); model.WritePointer(token, scriptStart + 2, scriptStart + 9); model.ObserveRunWritten(token, new PointerRun(scriptStart + 2)); var factory = new PCSRunContentStrategy(); factory.TryAddFormatAtDestination(model, token, scriptStart + 2, scriptStart + 9, default, default, default); eventModel.Graphics = trainerGraphics; eventModel.Elevation = 3; eventModel.MoveType = 2; eventModel.RangeXY = "(3, 3)"; eventModel.TrainerType = 0; eventModel.TrainerRangeOrBerryID = 0; eventModel.ScriptAddress = scriptStart; eventModel.Flag = 0; model.ObserveRunWritten(token, new XSERun(scriptStart, SortedSpan.One(eventModel.Start + 16))); } public int GetNPCTextPointer(IEventViewModel eventModel) => GetNPCTextPointer(model, eventModel); public static int GetNPCTextPointer(IDataModel model, IEventViewModel eventModel) { if (eventModel is not ObjectEventViewModel objectModel) return Pointer.NULL; var address = objectModel.ScriptAddress; if (address < 0) return Pointer.NULL; var expectedValues = new Dictionary { { 0, 0x0F }, { 1, 0x00 }, { 6, 0x09 }, { 7, 0x02 }, { 8, 0x02 }, }; if (address >= model.Count - expectedValues.Count) return Pointer.NULL; foreach (var (k, v) in expectedValues) { if (model[address + k] != v) return Pointer.NULL; } return address + 2; } #endregion #region Item public ObservableCollection ItemOptions { get; } = new(); private int itemID = 20; public int ItemID { get => itemID; set => Set(ref itemID, value); } public void CreateItem(ObjectEventViewModel objectEventModel, ModelDelta token) { // copyvarifnotzero 0x8000 item: // copyvarifnotzero 0x8001 1 // callstd 1 // end // item: var script = "1A 00 80 00 00 1A 01 80 01 00 09 01 02".ToByteArray(); var scriptStart = model.FindFreeSpace(model.FreeSpaceStart, script.Length); token.ChangeData(model, scriptStart, script); model.WriteMultiByteValue(scriptStart + 3, 2, token, itemID); var itemFlag = FindNextUnusedFlag(); objectEventModel.Graphics = ItemGraphics; objectEventModel.Elevation = 3; objectEventModel.MoveType = 8; objectEventModel.RangeX = objectEventModel.RangeY = 1; objectEventModel.TrainerType = objectEventModel.TrainerRangeOrBerryID = 0; objectEventModel.ScriptAddress = scriptStart; objectEventModel.Flag = itemFlag; model.ObserveRunWritten(token, new XSERun(scriptStart, SortedSpan.One(objectEventModel.Start + 16))); } public int GetItemAddress(IEventViewModel eventModel) => GetItemAddress(model, eventModel); public static int GetItemAddress(IDataModel model, IEventViewModel eventModel) { if (eventModel is not ObjectEventViewModel objectModel) return Pointer.NULL; var address = objectModel.ScriptAddress; return GetItemAddress(model, address); } public static int GetItemAddress(IDataModel model, int address) { if (address < 0) return Pointer.NULL; // 1A 00 80 item: 1A 01 80 01 00 09 01 02 var expectedValues = new Dictionary { { 0, 0x1A }, { 1, 0x00 }, { 2, 0x80 }, { 5, 0x1A }, { 6, 0x01 }, { 7, 0x80 }, { 8, 0x01 }, { 9, 0x00 }, { 10, 0x09 }, { 11, 0x01 }, }; if (address >= model.Count - expectedValues.Count) return Pointer.NULL; foreach (var (k, v) in expectedValues) { if (model[address + k] != v) return Pointer.NULL; } return address + 3; } private int ItemGraphics => model.IsFRLG() ? 92 : 59; #endregion #region Signpost public void ApplyTemplate(SignpostEventViewModel signpost, ModelDelta token) { if (signpost == null) return; signpost.Elevation = 0; signpost.Kind = 0; // loadpointer 0 ; callstd 3; end; text="Signpost Text" // 0F 00 09 03 02 "Signpost Text" var scriptStart = model.FindFreeSpace(model.FreeSpaceStart, 24); token.ChangeData(model, scriptStart, "0F 00 00 00 00 00 09 03 02 CD DD DB E2 E4 E3 E7 E8 00 CE D9 EC E8 FF".ToByteArray()); model.WritePointer(token, scriptStart + 2, scriptStart + 9); model.ObserveRunWritten(token, new PointerRun(scriptStart + 2)); var factory = new PCSRunContentStrategy(); factory.TryAddFormatAtDestination(model, token, scriptStart + 2, scriptStart + 9, default, default, default); // this XSE run has no pointer source, because the signpost Arg isn't always a pointer. model.ObserveRunWritten(token, new XSERun(scriptStart, SortedSpan.None)); signpost.Pointer = scriptStart; } public int GetSignpostTextPointer(IEventViewModel eventModel) => GetSignpostTextPointer(model, eventModel); public static int GetSignpostTextPointer(IDataModel model, IEventViewModel eventModel) { if (eventModel is not SignpostEventViewModel signpost) return Pointer.NULL; if (!signpost.ShowPointer) return Pointer.NULL; int address = signpost.Pointer; var expectedValues = new Dictionary { { 0, 0x0F }, { 1, 0x00 }, { 6, 0x09 }, { 7, 0x03 }, { 8, 0x02 }, }; foreach (var (k, v) in expectedValues) { if (address + k < 0 || address + k >= model.Count) return Pointer.NULL; if (model[address + k] != v) return Pointer.NULL; } return address + 2; } #endregion #region Mart private int ClerkGraphics => model.IsFRLG() ? 68 : 83; public void CreateMart(ObjectEventViewModel objectEventViewModel, ModelDelta token) { // FireRed template: // lock faceplayer preparemsg waitmsg pokemart loadpointer 0 msg callstd 4 release end // 6A 5A 67 11 62 1A 08 66 86 08 A7 16 08 0F 00 90 51 1A 08 09 04 6C 02 // 0x20 bytes total // pokeball/potion/antidote var script = "6A 5A 67 00 00 00 00 66 86 00 00 00 00 0F 00 00 00 00 00 09 04 6C 02 FF 04 00 0D 00 0E 00 00 00".ToByteArray(); // 3 pointer to start text // 9 pointer to mart // 15 pointer to end text // 24 start of mart data var hello = WriteText(token, "Hi, there!\\nMay I help you?"); var goodbye = WriteText(token, "Please come again!"); var scriptStart = model.FindFreeSpace(model.FreeSpaceStart, script.Length); token.ChangeData(model, scriptStart, script); model.WritePointer(token, scriptStart + 3, hello); model.WritePointer(token, scriptStart + 9, scriptStart + 24); model.WritePointer(token, scriptStart + 15, goodbye); objectEventViewModel.Graphics = ClerkGraphics; objectEventViewModel.Elevation = 3; objectEventViewModel.MoveType = 10; objectEventViewModel.RangeX = objectEventViewModel.RangeY = 0; objectEventViewModel.TrainerType = objectEventViewModel.TrainerRangeOrBerryID = objectEventViewModel.Flag = 0; objectEventViewModel.ScriptAddress = scriptStart; model.ObserveRunWritten(token, new XSERun(scriptStart, SortedSpan.One(objectEventViewModel.Start + 16))); parser.WriteMartStream(model, token, scriptStart + 24, scriptStart + 9); foreach (var offset in new[] { 3, 9, 15 }) model.ObserveRunWritten(token, new PointerRun(scriptStart + offset)); } public MartEventContent GetMartContent(ObjectEventViewModel eventModel) => GetMartContent(model, parser, eventModel); public static MartEventContent GetMartContent(IDataModel model, ScriptParser parser, ObjectEventViewModel eventViewModel) { var spots = Flags.GetAllScriptSpots(model, parser, new[] { eventViewModel.ScriptAddress }, 0x67, 0x86, 0x0F); // preparemsg, pokemart, loadpointer // look for the first preparemsg, then the first pokemart, then the first loadpointer var results = spots.GetEnumerator(); if (!results.MoveNext()) return null; var messageStart = results.Current; if (model[messageStart.Address] != 0x67) return null; if (!results.MoveNext()) return null; var martStart = results.Current; if (model[martStart.Address] != 0x86) return null; if (!results.MoveNext()) return null; var loadStart = results.Current; if (model[loadStart.Address] != 0x0F) return null; var messageAddress = model.ReadPointer(messageStart.Address + 1); var martAddress = model.ReadPointer(martStart.Address + 1); var loadAddress = model.ReadPointer(loadStart.Address + 2); if (messageAddress < 0 || messageAddress >= model.Count) return null; if (martAddress < 0 || martAddress >= model.Count) return null; if (loadAddress < 0 || loadAddress >= model.Count) return null; return new(messageStart.Address + 1, martStart.Address + 1, loadStart.Address + 2); } #endregion #region Tutor public void CreateTutor(ObjectEventViewModel objectEventViewModel, ModelDelta token) { /* pseudo code: * if flag: goto end * print "forward text" -> yes/no * if no: goto failed * print "only can learn once!" -> yes/no * if no: goto failed * print "which pokemon will learn?" * ChooseMonForMoveTutor * if no: goto failed * setflag * end: * print "done text" * end * failed: * print "failed text" * end */ var tutorFlag = FindNextUnusedFlag(); int tutor = 0; int infoStart = WriteText(token, "Want to learn a cool move?"); int warningStart = WriteText(token, "This move can be learned only\\nonce. Is that okay?"); int whichStart = WriteText(token, "Which POKéMON wants to learn\\nthe move?"); int doneStart = WriteText(token, "Enjoy the move!"); int failedStart = WriteText(token, "I guess not."); var fr = model.IsFRLG(); var script = $@" lock faceplayer checkflag {tutorFlag} if1 = loadpointer 0 <{infoStart:X6}> callstd 5 compare 0x800D 0 if1 = {(fr?"textcolor 3":string.Empty)} {(fr?"special DisableMsgBoxWalkaway":string.Empty)} {(fr?"signmsg":string.Empty)} loadpointer 0 <{warningStart:X6}> callstd 5 {(fr?"normalmsg":string.Empty)} copyvar 0x8012 0x8013 campare 0x800D 0 if1 = loadpointer 0 <{whichStart:X6}> callstd 4 setvar 0x8005 {tutor} special ChooseMonForMoveTutor waitstate lock faceplayer compare 0x800D 0 if1 = setflag {tutorFlag} success: loadpointer 0 <{doneStart:X6}> callstd 4 release end failed: loadpointer 0 <{failedStart:X6}> callstd 4 release end "; // script length = 109 // note that the condition for recognizing the `warningStart` message is different in // the Emerald case, since there's no `signmsg` command to use for reference // instead, it's just 0 or 1 pointers, and has `callstd 5` after it. // maybe just expect a `callstd 5` after it, since it's the only one after infoStart that has that in both FR and Em var scriptStart = model.FindFreeSpace(model.FreeSpaceStart, 109); var content = parser.Compile(token, model, scriptStart, ref script, out var _, out var _); token.ChangeData(model, scriptStart, content); objectEventViewModel.Graphics = trainerGraphics; objectEventViewModel.Elevation = 3; objectEventViewModel.MoveType = 8; objectEventViewModel.RangeX = objectEventViewModel.RangeY = 0; objectEventViewModel.TrainerType = objectEventViewModel.TrainerRangeOrBerryID = 0; objectEventViewModel.ScriptAddress = scriptStart; objectEventViewModel.Flag = 0; model.ObserveRunWritten(token, new XSERun(scriptStart, SortedSpan.One(objectEventViewModel.Start + 16))); parser.FormatScript(token, model, scriptStart); } public TutorEventContent GetTutorContent(ScriptParser parser, ObjectEventViewModel eventModel) => GetTutorContent(model, parser, eventModel); public static TutorEventContent GetTutorContent(IDataModel model, ScriptParser parser, ObjectEventViewModel eventViewModel) { // tutors must have a `special ChooseMonForMoveTutor` if (!model.TryGetList("specials", out var specials)) return null; var tutorSpecial = specials.IndexOf("ChooseMonForMoveTutor"); if (tutorSpecial == -1) return null; if (!Flags.GetAllScriptSpots( model, parser, new[] { eventViewModel.ScriptAddress }, 0x25 ).Any( spot => model.ReadMultiByteValue(spot.Address + 1, 2) == tutorSpecial) ) return null; var content = new TutorEventContent(Pointer.NULL, Pointer.NULL, Pointer.NULL, Pointer.NULL, Pointer.NULL); var spots = Flags.GetAllScriptSpots(model, parser, new[] { eventViewModel.ScriptAddress }, 0x16, 0x0F); // setvar, loadpointer foreach (var spot in spots) { if (model[spot.Address] == 0x16) { if (content.TutorAddress != Pointer.NULL) return null; if (model.ReadMultiByteValue(spot.Address + 1, 2) != 0x8005) return null; content = content with { TutorAddress = spot.Address + 3 }; continue; } if (content.InfoPointer == Pointer.NULL) { content = content with { InfoPointer = spot.Address + 2 }; if (!GoodPointer(model, spot.Address + 2)) return null; continue; } if (model[spot.Address + 6] == 9 && model[spot.Address + 7] == 5) continue; // skip warningpointer (has a `callstd 5` after it) // it's either which, success, or fail. We can tell by the number of pointers var run = model.GetNextRun(spot.Address); if (spot.Address != run.Start) { // 0 pointers -> WhichPokemon if (content.WhichPokemonPointer != Pointer.NULL) return null; content = content with { WhichPokemonPointer = spot.Address + 2 }; if (!GoodPointer(model, spot.Address + 2)) return null; continue; } if (run.PointerSources == null) return null; if (run.PointerSources.Count == 3) { // 3 pointers -> Failed if (content.FailedPointer != Pointer.NULL) return null; content = content with { FailedPointer = spot.Address + 2 }; if (!GoodPointer(model, spot.Address + 2)) return null; continue; } if (run.PointerSources.Count.IsAny(1, 2)) { // 1 or 2 pointers -> Success if (content.SuccessPointer == spot.Address + 2) continue; if (content.SuccessPointer != Pointer.NULL) return null; content = content with { SuccessPointer = spot.Address + 2 }; if (!GoodPointer(model, spot.Address + 2)) return null; continue; } return null; } if (Pointer.NULL.IsAny(content.InfoPointer, content.WhichPokemonPointer, content.SuccessPointer, content.FailedPointer, content.TutorAddress)) return null; return content; } #endregion #region Trade public void CreateTrade(ObjectEventViewModel objectEventViewModel, ModelDelta token) { var tradeFlag = FindNextUnusedFlag(); int tradeId = 0; int initialText = WriteText(token, "Want to trade your \\\\02\nfor my \\\\03?"); int thanksText = WriteText(token, "Thank you!"); int successText = WriteText(token, "How is my old \\\\03?\\pnYour old \\\\02 is doing great!"); int failText = WriteText(token, "That's too bad."); int wrongSpeciesText = WriteText(token, "\\.This is no \\\\02.\\pnIf you get one, please trade it\\nfor my \\\\03!"); var script = @$" lock faceplayer setvar 0x8008 {tradeId} copyvar 0x8004 0x8008 special2 0x800D GetInGameTradeSpeciesInfo copyvar 0x8009 0x800D checkflag {tradeFlag} if1 = loadpointer 0 <{initialText:X6}> callstd 5 compare 0x800D 0 if1 = special ChoosePartyMon waitstate lock faceplayer copyvar 0x800A 0x8004 compare 0x8004 6 if1 >= copyvar 0x8005 0x800A special2 0x800D GetTradeSpecies copyvar 0x800B 0x800D comparevars 0x800D 0x8009 if1 != copyvar 0x8004 0x8008 copyvar 0x8005 0x800A special CreateInGameTradePokemon special DoInGameTradeScene waitstate lock faceplayer loadpointer 0 <{thanksText:X6}> callstd 4 setflag {tradeFlag} release end success: loadpointer 0 <{successText:X6}> callstd 4 release end fail: loadpointer 0 <{failText:X6}> callstd 4 release end wrongspecies: loadpointer 0 <{wrongSpeciesText:X6}> callstd 4 release end "; var scriptStart = model.FindFreeSpace(model.FreeSpaceStart, 160); var content = parser.Compile(token, model, scriptStart, ref script, out var _, out var _); token.ChangeData(model, scriptStart, content); objectEventViewModel.Graphics = trainerGraphics; objectEventViewModel.Elevation = 3; objectEventViewModel.MoveType = 8; objectEventViewModel.RangeX = objectEventViewModel.RangeY = 0; objectEventViewModel.TrainerType = objectEventViewModel.TrainerRangeOrBerryID = 0; objectEventViewModel.ScriptAddress = scriptStart; objectEventViewModel.Flag = 0; model.ObserveRunWritten(token, new XSERun(scriptStart, SortedSpan.One(objectEventViewModel.Start + 16))); parser.FormatScript(token, model, scriptStart); } public TradeEventContent GetTradeEventContent(ScriptParser parser, ObjectEventViewModel eventModel) => GetTradeContent(model, parser, eventModel.ScriptAddress); public static TradeEventContent GetTradeContent(IDataModel model, ScriptParser parser, int scriptAddress) { // tardes must have a `special CreateInGameTradePokemon` if (!model.TryGetList("specials", out var specials)) return null; var tradeSpecial = specials.IndexOf("CreateInGameTradePokemon"); if (tradeSpecial == -1) return null; if (!Flags.GetAllScriptSpots( model, parser, new[] { scriptAddress }, 0x25 ).Any( spot => model.ReadMultiByteValue(spot.Address + 1, 2) == tradeSpecial) ) return null; var content = new TradeEventContent(Pointer.NULL, Pointer.NULL, Pointer.NULL, Pointer.NULL, Pointer.NULL, Pointer.NULL); var spots = Flags.GetAllScriptSpots(model, parser, new[] { scriptAddress }, 0x16, 0x0F); // setvar, loadpointer foreach (var spot in spots) { // TradeAddress is the only `setvar 0x8008` command if (model[spot.Address] == 0x16) { if (model.ReadMultiByteValue(spot.Address + 1, 2) != 0x8008) continue; if (content.TradeAddress != Pointer.NULL) return null; content = content with { TradeAddress = spot.Address + 3 }; continue; } // loadpointer InfoPointer is right before callstd 5 if (model[spot.Address + 7] == 5) { if (content.InfoPointer != Pointer.NULL) return null; content = content with { InfoPointer = spot.Address + 2 }; continue; } // ThanksPointer, SuccessPointer, FailedPointer, and WrongSpecies all look exactly the same, but come in that order if (content.ThanksPointer == Pointer.NULL) { content = content with { ThanksPointer = spot.Address + 2 }; continue; } else if (content.SuccessPointer == Pointer.NULL) { content = content with { SuccessPointer = spot.Address + 2 }; continue; } else if (content.FailedPointer == Pointer.NULL) { content = content with { FailedPointer = spot.Address + 2 }; continue; } else if (content.WrongSpeciesPointer == Pointer.NULL) { content = content with { WrongSpeciesPointer = spot.Address + 2 }; continue; } return null; } if (Pointer.NULL.IsAny(content.InfoPointer, content.ThanksPointer, content.SuccessPointer, content.FailedPointer, content.WrongSpeciesPointer, content.TradeAddress)) return null; return content; } #endregion #region Legendary Encounter public void CreateLegendary(ObjectEventViewModel objectEventModel, ModelDelta token) { var legendFlag = FindNextUnusedFlag(); var catchFlag = FindNextUnusedFlag(); int cryText = model.IsFRLG() ? WriteText(token, "Roar!") : Pointer.NULL; #region script var script = new StringBuilder(@" lock faceplayer waitsound cry 1 2 setwildbattle 1 50 0 "); if (model.IsFRLG()) { script.AppendLine($"preparemsg <{cryText:X6}>"); script.AppendLine("waitmsg"); } script.AppendLine("waitcry"); script.AppendLine("pause 10"); if (model.IsFRLG()) { script.AppendLine(@" playsong mus_encounter_gym_leader playOnce waitkeypress setflag 0x0807 special 0x138 waitstate clearflag 0x0807 "); } else if (model.IsEmerald()) { script.AppendLine(@" setflag 0x08C1 special 0x13B waitstate clearflag 0x08C1 "); } else { script.AppendLine(@" setflag 0x861 special 0x137 waitstate clearflag 0x861 "); } script.AppendLine(@$" fadescreen 1 hidesprite 0x800F fadescreen 0 special2 0x800D GetBattleOutcome if.compare.goto 0x800D = 7 bufferPokemon 0 1 msgbox.default {{ The [buffer1] disappeared! }} release end caught: setflag 0x{catchFlag:X4} release end "); #endregion var scriptStart = model.FindFreeSpace(model.FreeSpaceStart, 160); var scriptText = script.ToString(); var content = parser.Compile(token, model, scriptStart, ref scriptText, out var _, out var _); token.ChangeData(model, scriptStart, content); objectEventModel.Graphics = trainerGraphics; objectEventModel.Elevation = FindPreferredTrainerElevation(model, trainerGraphics); objectEventModel.MoveType = 8; objectEventModel.RangeX = objectEventModel.RangeY = 0; objectEventModel.TrainerType = objectEventModel.TrainerRangeOrBerryID = 0; objectEventModel.ScriptAddress = scriptStart; objectEventModel.Flag = legendFlag; model.ObserveRunWritten(token, new XSERun(scriptStart, SortedSpan.One(objectEventModel.Start + 16))); parser.FormatScript(token, model, scriptStart); } public LegendaryEventContent GetLegendaryEventContent(ScriptParser parser, ObjectEventViewModel eventModel) => GetLegendaryEventContent(model, parser, eventModel); public static LegendaryEventContent GetLegendaryEventContent(IDataModel model, ScriptParser parser, ObjectEventViewModel ev) { var content = new LegendaryEventContent(Pointer.NULL, Pointer.NULL, null, null, Pointer.NULL); /* 67 preparemsg text<""> A1 cry species:data.pokemon.names effect: B6 setwildbattle species: level. item: 29 setflag flag: 2A clearflag flag: 7D bufferPokemon buffer.3 species:data.pokemon.names */ var spots = Flags.GetAllScriptSpots(model, parser, new[] { ev.ScriptAddress }, 0x67, 0xA1, 0xB6, 0x29, 0x2A, 0x7D); var flagsSet = new Dictionary(); // address of flag -> flag value var flagsCleared = new Dictionary(); // address of flag -> flag value var bufferSpots = new Dictionary(); // address of buffer -> pokemon to buffer foreach (var spot in spots) { if (spot.Line.LineCode[0] == 0x29) { flagsSet[spot.Address + 1] = model.ReadMultiByteValue(spot.Address + 1, 2); } else if (spot.Line.LineCode[0] == 0x2A) { flagsCleared[spot.Address + 1] = model.ReadMultiByteValue(spot.Address + 1, 2); } else if (spot.Line.LineCode[0] == 0x7D) { bufferSpots[spot.Address + 2] = model.ReadMultiByteValue(spot.Address + 2, 2); } else { content = spot.Line.LineCode[0] switch { 0x67 => content with { CryTextPointer = spot.Address + 1 }, 0xA1 => content with { Cry = spot.Address }, 0xB6 => content with { SetWildBattle = spot.Address }, _ => throw new NotImplementedException(), }; } } if (content.Cry == Pointer.NULL) return null; if (content.SetWildBattle == Pointer.NULL) return null; var setOnlyFlags = flagsSet.Values.Except(flagsCleared.Values).ToHashSet(); if (setOnlyFlags.Count != 1) return null; var bufferPokemon = new List(); foreach (var kvp in bufferSpots) { if (kvp.Value == model.ReadMultiByteValue(content.SetWildBattle + 1, 2)) bufferPokemon.Add(kvp.Key - 2); } var legendFlag = setOnlyFlags.Single(); var legendFlagAddress = flagsSet.Keys.Where(key => flagsSet[key] == legendFlag); content = content with { SetFlag = legendFlagAddress.Select(flag => flag - 1).ToList() }; content = content with { BufferPokemon = bufferPokemon }; return content; } #endregion #region HM Object public ObservableCollection HMObjectOptions { get; } = new(); private int hmObjectIndex; public int HMObjectIndex { get => hmObjectIndex; set { Set(ref hmObjectIndex, value); UpdateSpriteFromHMObject(); } } private void UpdateSpriteFromHMObject() { if (hmObjectIndex < 0 || hmObjectIndex > 2) return; // FR/LG: 95, 96, 97 // R/S/EE: 82, 86, 87 if (model.IsFRLG()) TrainerGraphics = new[] { 95, 96, 97 }[hmObjectIndex]; else TrainerGraphics = new[] { 82, 86, 87 }[hmObjectIndex]; } public void CreateHMObject(ObjectEventViewModel objectEventViewModel, ModelDelta token) { var scriptStart = AllMapsModel.Create(model, default) .SelectMany(bank => bank) .SelectMany(map => map?.Events.Objects ?? new()) .Where(obj => obj.Graphics == trainerGraphics) .Select(obj => obj.ScriptAddress) .ToHistogram() .MostCommonKey(); objectEventViewModel.Graphics = trainerGraphics; objectEventViewModel.Elevation = 3; objectEventViewModel.MoveType = 8; objectEventViewModel.RangeX = objectEventViewModel.RangeY = 0; objectEventViewModel.TrainerType = objectEventViewModel.TrainerRangeOrBerryID = 0; objectEventViewModel.ScriptAddress = scriptStart; objectEventViewModel.Flag = (objectEventViewModel.ObjectID % 0x10) + 0x10; } #endregion #region Helper Methods private static bool GoodPointer(IDataModel model, int address) { if (address < 0 || address >= model.Count - 3) return false; address = model.ReadPointer(address); return 0 <= address && address < model.Count; } private int WriteText(ModelDelta token, string text) { var bytes = model.TextConverter.Convert(text, out var _); var start = model.FindFreeSpace(model.FreeSpaceStart, bytes.Count); token.ChangeData(model, start, bytes); return start; } private void UpdateObjectTemplateImage(TemplateType old = default) { if (selectedTemplate == TemplateType.None) { ObjectTemplateImage = GraphicsOptions[0]; } else if (selectedTemplate.IsAny(TemplateType.Trainer, TemplateType.Npc, TemplateType.Tutor, TemplateType.Trade, TemplateType.Legendary, TemplateType.HMObject)) { ObjectTemplateImage = GraphicsOptions[TrainerGraphics]; } else if (selectedTemplate == TemplateType.Item) { ObjectTemplateImage = GraphicsOptions[ItemGraphics]; } else if (selectedTemplate == TemplateType.Mart) { ObjectTemplateImage = GraphicsOptions[ClerkGraphics]; } if (ObjectTemplateImage.PixelData.Length > 0) { ObjectTemplateImage = new ReadonlyPixelViewModel(ObjectTemplateImage.PixelWidth, ObjectTemplateImage.PixelHeight, ObjectTemplateImage.PixelData, ObjectTemplateImage.PixelData[0]); } ObjectTemplateImage = ObjectTemplateImage.AutoCrop(); NotifyPropertyChanged(nameof(ObjectTemplateImage)); } #endregion } public record TrainerEventContent(int BeforeTextPointer, int WinTextPointer, int AfterTextPointer, int TrainerClassAddress, int TrainerIndex, int TrainerIndexAddress, int TrainerNameAddress, int TeamPointer); public record MartEventContent(int HelloPointer, int MartPointer, int GoodbyePointer); public record TutorEventContent(int InfoPointer, int WhichPokemonPointer, int FailedPointer, int SuccessPointer, int TutorAddress); public record TradeEventContent(int InfoPointer, int ThanksPointer, int SuccessPointer, int FailedPointer, int WrongSpeciesPointer, int TradeAddress); public record LegendaryEventContent(int Cry, int SetWildBattle, List BufferPokemon, List SetFlag, int CryTextPointer); public enum TemplateType { None, Npc, Item, Trainer, Mart, Tutor, Trade, Legendary, HMObject } } /* * FireRed flags that get missed by the current algorithm: // 2A2 visited sevii island 2 // 2A7 -> aurora ticket // 2A8 -> mystic ticket // 2CF -> visited Oak's Lab // 2D2/2D3 -> seafoam B3F/B4F current // 2DE -> tutor frezy plant? // 2DF -> tutor blast burn? // 2E0 -> tutor hydro cannon? // missing 3E8 to 4A6 (hidden items) // missing 4BC -> defeat champ * known gaps in the current algorithm: * -> doesn't check map header scripts * -> doesn't understand flags that are set using variables */