mirror of
https://github.com/haven1433/HexManiacAdvance.git
synced 2026-05-14 00:00:36 -05:00
This will allow all tabs, between maps, viewports, and duplicates, to track flag/var use together.
1116 lines
48 KiB
C#
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
|
|
*/
|