HexManiacAdvance/src/HexManiac.Core/ViewModels/Map/EventTemplate.cs
haven1433 dedc848ba9 map tabs of the same model should share the same EventTemplate object.
This will allow all tabs, between maps, viewports, and duplicates, to track flag/var use together.
2023-08-12 23:03:17 -05:00

1116 lines
48 KiB
C#

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<int> usedFlags, usedTrainerFlags, usedVariables;
private IReadOnlyDictionary<int, TrainerPreference> trainerPreferences;
private IReadOnlyDictionary<int, int> minLevel;
private ISet<int> UsedFlags {
get {
initializationWorkload.Wait();
return usedFlags;
}
}
private ISet<int> UsedTrainerFlags {
get {
initializationWorkload.Wait();
return usedTrainerFlags;
}
}
private ISet<int> UsedVariables {
get {
initializationWorkload.Wait();
return usedVariables;
}
}
public void UseTrainerFlag(int flag) => UsedTrainerFlags.Add(flag);
public bool IsTrainerFlagInUse(int flag) => UsedTrainerFlags.Contains(flag);
private IReadOnlyDictionary<int, TrainerPreference> TrainerPreferences {
get {
if (trainerPreferences == null) trainerPreferences = Flags.GetTrainerPreference(model, parser);
return trainerPreferences;
}
}
private IReadOnlyDictionary<int, int> MinLevel {
get {
if (minLevel == null) minLevel = Flags.GetMinimumLevelForPokemon(model);
return minLevel;
}
}
public IPixelViewModel ObjectTemplateImage { get; private set; }
public IReadOnlyList<IPixelViewModel> OverworldGraphics { get; private set; }
public EventTemplate(IWorkDispatcher dispatcher, IDataModel model, ScriptParser parser, IReadOnlyList<IPixelViewModel> 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<IPixelViewModel> 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<ComboOption>();
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<TemplateType> 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<VisualComboOption> GraphicsOptions { get; } = new();
public ObservableCollection<string> 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<int>();
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 <before> <during>
loadpointer 0 <after>
callstd 6
end
*/
// 2 6 10 16
// 5C 00 trainerFlag: 00 00 <before> <win> 0F 00 <after> 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<int, int>();
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 <before> <win> 0F 00 <after> 09 06 02
var expectedValues = new Dictionary<int, byte> {
{ 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 <text>; callstd 2; end; text="I'm an NPC!"
// 0F 00 <text> 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<int, byte> {
{ 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<string> 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<int, byte> {
{ 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 <text>; callstd 3; end; text="Signpost Text"
// 0F 00 <text> 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<int>.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<int, byte> {
{ 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
// <pointer> <pointer> <pointer> 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 = <success>
loadpointer 0 <{infoStart:X6}>
callstd 5
compare 0x800D 0
if1 = <failed>
{(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 = <failed>
loadpointer 0 <{whichStart:X6}>
callstd 4
setvar 0x8005 {tutor}
special ChooseMonForMoveTutor
waitstate
lock
faceplayer
compare 0x800D 0
if1 = <failed>
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<XSERun>(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 = <success>
loadpointer 0 <{initialText:X6}>
callstd 5
compare 0x800D 0
if1 = <fail>
special ChoosePartyMon
waitstate
lock
faceplayer
copyvar 0x800A 0x8004
compare 0x8004 6
if1 >= <fail>
copyvar 0x8005 0x800A
special2 0x800D GetTradeSpecies
copyvar 0x800B 0x800D
comparevars 0x800D 0x8009
if1 != <wrongspecies>
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<XSERun>(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 <caught>
bufferPokemon 0 1
msgbox.default <auto>
{{
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<XSERun>(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<int, int>(); // address of flag -> flag value
var flagsCleared = new Dictionary<int, int>(); // address of flag -> flag value
var bufferSpots = new Dictionary<int, int>(); // 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<int>();
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<string> 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<int> BufferPokemon, List<int> 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
*/