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.Sprites; using HavenSoft.HexManiac.Core.ViewModels.DataFormats; using HavenSoft.HexManiac.Core.ViewModels.Images; using HavenSoft.HexManiac.Core.ViewModels.Tools; using Microsoft.Scripting.Utils; using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using System.Linq; using System.Runtime.CompilerServices; using System.Windows.Input; namespace HavenSoft.HexManiac.Core.ViewModels.Map { public interface IEventViewModel : IEquatable, INotifyPropertyChanged { event EventHandler EventVisualUpdated; public event EventHandler CycleEvent; public ICommand CycleEventCommand { get; } public ModelArrayElement Element { get; } string EventType { get; } string EventIndex { get; } int TopOffset { get; } int LeftOffset { get; } int X { get; set; } int Y { get; set; } IPixelViewModel EventRender { get; } void Render(IDataModel model, LayoutModel layout); bool Delete(); } public enum EventCycleDirection { PreviousCategory, PreviousEvent, NextEvent, NextCategory, None } public class FlyEventViewModel : ViewModelCore, IEventViewModel { private readonly ModelArrayElement flySpot; private readonly ModelArrayElement connectionEntry; public string EventType => "Fly"; public string EventIndex => "1/1"; public virtual int TopOffset => 0; public virtual int LeftOffset => 0; public ModelArrayElement Element => flySpot; #region X/Y public int X { get => !Valid ? -1 : flySpot.GetValue("x"); set { if (!Valid) return; flySpot.SetValue("x", value); NotifyPropertyChanged(); if (!ignoreUpdateXY) xy = null; NotifyPropertyChanged(nameof(XY)); EventVisualUpdated.Raise(this); } } public int Y { get => !Valid ? -1 : flySpot.GetValue("y"); set { if (!Valid) return; flySpot.SetValue("y", value); NotifyPropertyChanged(); if (!ignoreUpdateXY) xy = null; NotifyPropertyChanged(nameof(XY)); EventVisualUpdated.Raise(this); } } private bool ignoreUpdateXY; private string xy; public string XY { get { if (!Valid) return "(-1, -1)"; if (xy == null) xy = $"({X}, {Y})"; return xy; } set { if (!Valid) return; xy = value; var parts = value.Split(new[] { ',', ' ', '(', ')' }, StringSplitOptions.RemoveEmptyEntries); if (parts.Length != 2) return; ignoreUpdateXY = true; if (parts[0].TryParseInt(out int x)) X = x; if (parts[1].TryParseInt(out int y)) Y = y; ignoreUpdateXY = false; } } #endregion public IPixelViewModel EventRender { get; private set; } public bool Valid { get; } private StubCommand cycleEventCommand; public ICommand CycleEventCommand => StubCommand(ref cycleEventCommand, direction => { CycleEvent.Raise(this, direction); }); public event EventHandler EventVisualUpdated; public event EventHandler CycleEvent; public static IEnumerable Create(IDataModel model, int bank, int map, Func tokenFactory) { var flyTable = model.GetTableModel(HardcodeTablesModel.FlySpawns, tokenFactory); if (flyTable == null) yield break; for (int i = 0; i < flyTable.Count; i++) { var flight = flyTable[i]; if (flight.GetValue("bank") != bank) continue; if (flight.GetValue("map") != map) continue; yield return new FlyEventViewModel(flight, bank, map, i + 1); } } public FlyEventViewModel(ModelArrayElement flySpot, int bank, int map, int expectedFlight) { this.flySpot = flySpot; var model = flySpot.Model; var tokenFactory = () => flySpot.Token; // get the region from the map var banks = model.GetTableModel(HardcodeTablesModel.MapBankTable, tokenFactory); if (banks == null) return; // not valid map table var maps = banks[bank].GetSubTable("maps"); if (maps == null) return; // not valid bank var table = maps[map].GetSubTable("map"); if (table == null) return; // not valid map var region = table[0].GetValue(Format.RegionSection); if (model.IsFRLG()) region -= 88; if (region < 0) return; // not valid region section Valid = true; // connection entries are optional var flyIndexTable = model.GetTableModel(HardcodeTablesModel.FlyConnections, tokenFactory); if (flyIndexTable == null) return; if (region >= flyIndexTable.Count) return; var entry = flyIndexTable[region]; if (entry.TryGetValue("flight", out int savedFlight) && savedFlight == expectedFlight) { connectionEntry = entry; } } /// true if the event was deleted public bool Delete() { if (!Valid) return false; if (connectionEntry == null) return false; // cannot delete 'special' fly events (such as the player's house) // set the connection table's index to 0 connectionEntry.SetValue("flight", 0); flySpot.SetValue("bank", 0); flySpot.SetValue("map", 0); flySpot.SetValue("x", 0); flySpot.SetValue("y", 0); return true; } public bool Equals(IEventViewModel? other) { if (other is not FlyEventViewModel fly) return false; return X == fly.X && Y == fly.Y && flySpot.Start == fly.flySpot.Start; } public void Render(IDataModel model, LayoutModel layout) { EventRender = BaseEventViewModel.BuildEventRender(UncompressedPaletteColor.Pack(31, 31, 0)); } } public abstract class BaseEventViewModel : ViewModelCore, IEventViewModel, IEquatable { public event EventHandler EventVisualUpdated; public event EventHandler CycleEvent; private StubCommand cycleEventCommand; public ICommand CycleEventCommand => StubCommand(ref cycleEventCommand, direction => { CycleEvent?.Invoke(this, direction); }); protected readonly ModelArrayElement element; private readonly string parentLengthField; public ModelArrayElement Element => element; public ModelDelta Token => element.Token; public string EventType => GetType().Name.Replace("EventViewModel", " Event"); public string EventIndex => $"{element.ArrayIndex + 1} / {element.Table.ElementCount}"; public virtual int TopOffset => 0; public virtual int LeftOffset => 0; #region X/Y public int X { get => element.GetValue("x"); set { element.SetValue("x", value); NotifyPropertyChanged(); if (!ignoreUpdateXY) xy = null; NotifyPropertyChanged(nameof(XY)); RaiseEventVisualUpdated(); } } public int Y { get => element.GetValue("y"); set { element.SetValue("y", value); NotifyPropertyChanged(); if (!ignoreUpdateXY) xy = null; NotifyPropertyChanged(nameof(XY)); RaiseEventVisualUpdated(); } } private bool ignoreUpdateXY; private string xy; public string XY { get { if (xy == null) xy = $"({X}, {Y})"; return xy; } set { xy = value; var parts = value.Split(new[] { ',', ' ', '(', ')' }, StringSplitOptions.RemoveEmptyEntries); if (parts.Length != 2) return; ignoreUpdateXY = true; if (parts[0].TryParseInt(out int x)) X = x; if (parts[1].TryParseInt(out int y)) Y = y; ignoreUpdateXY = false; } } #endregion #region Elevation public int Elevation { get => element.GetValue("elevation"); set => element.SetValue("elevation", value); } #endregion public IPixelViewModel EventRender { get; protected set; } public BaseEventViewModel(ModelArrayElement element, string parentLengthField) => (this.element, this.parentLengthField) = (element, parentLengthField); public bool Delete() => DeleteElement(parentLengthField); public virtual bool Equals(IEventViewModel other) { if (other is not BaseEventViewModel bem) return false; return bem.element.Start == element.Start; } public abstract void Render(IDataModel model, LayoutModel layout); protected void RaiseEventVisualUpdated() => EventVisualUpdated.Raise(this); protected bool DeleteElement(string parentCountField) { var table = element.Table; var model = element.Model; var token = element.Token; var offset = table.ConvertByteOffsetToArrayOffset(element.Start); var editCount = table.ElementCount - offset.ElementIndex - 1; for (int i = 0; i < editCount; i++) { int segmentOffset = 0; for (int j = 0; j < table.ElementContent.Count; j++) { var source = element.Start + (i + 1) * element.Length + segmentOffset; var destination = source - element.Length; var length = table.ElementContent[j].Length; if (table.ElementContent[j].Type == ElementContentType.Pointer) { model.UpdateArrayPointer(token, table.ElementContent[j], table.ElementContent, offset.ElementIndex, destination, model.ReadPointer(source)); } else { model.WriteMultiByteValue(destination, length, token, model.ReadMultiByteValue(source, length)); } segmentOffset += length; } } if (table.ElementCount > 1) { var shorterTable = table.Append(token, -1); model.ObserveRunWritten(token, shorterTable); } else { foreach (var source in table.PointerSources) { model.UpdateArrayPointer(token, null, null, 0, source, Pointer.NULL); if (model.GetNextRun(source) is ITableRun parentTable) { var parent = new ModelArrayElement(model, parentTable.Start, 0, () => token, parentTable); parent.SetValue(parentCountField, 0); } } model.ClearFormatAndData(token, table.Start, table.Length); } return true; } protected string GetText(int pointer) { if (pointer == Pointer.NULL) return null; var address = element.Model.ReadPointer(pointer); if (address < 0 || address >= element.Model.Count) return null; var run = element.Model.GetNextRun(address); if (run.Start != address) { if (run.Start < address) return null; var length = PCSString.ReadString(element.Model, address, true); if (run.Start < address + length || length < 1) return null; // we can add a PCSRun here run = new PCSRun(element.Model, address, length, SortedSpan.One(pointer)); if (element.Model.GetNextRun(pointer).Start >= pointer + 4) element.Model.ObserveRunWritten(element.Token, new PointerRun(pointer)); element.Model.ObserveRunWritten(element.Token, run); } if (run is not PCSRun pcs) { var length = PCSString.ReadString(element.Model, address, true); element.Model.ClearFormat(element.Token, address, length); pcs = new PCSRun(element.Model, address, length, run.PointerSources); } if (pcs.Length < 1) return string.Empty; return pcs.SerializeRun(); } protected int SetText(int pointer, string text, [CallerMemberName] string propertyName = null) { if (pointer == Pointer.NULL) return Pointer.NULL; var address = element.Model.ReadPointer(pointer); if (address < 0 || address >= element.Model.Count) return -1; if (element.Model.GetNextRun(address) is not PCSRun pcs) return -1; var newRun = pcs.DeserializeRun(text, element.Token, out _, out _); element.Model.ObserveRunWritten(element.Token, newRun); NotifyPropertyChanged(propertyName); return newRun.Start != pcs.Start ? newRun.Start : -1; } protected string GetAddressText(int address, ref string field) { if (field == null) { field = $"<{address.ToAddress()}>"; if (address == Pointer.NULL) field = ""; } return field; } protected void SetAddressText(string value, ref string field, string fieldName) { field = value; value = field.Trim(" ".ToCharArray()); element.SetAddress(fieldName, value.TryParseHex(out int result) ? result : Pointer.NULL); } private static readonly Point[] focalPoints = new[] { new Point(0, 7), new Point(7, 0), new Point(15, 8), new Point(8, 15) }; public static IPixelViewModel BuildEventRender(short color, bool indentSides = false) { var pixels = new short[256]; for (int x = 1; x < 15; x++) { for (int y = 1; y < 15; y++) { if (((x + y) & 1) != 0) continue; if (indentSides && focalPoints.Any(p => Math.Abs(p.X - x) + Math.Abs(p.Y - y) < 4)) continue; pixels[y * 16 + x] = color; y++; } } return new ReadonlyPixelViewModel(new SpriteFormat(4, 2, 2, default), pixels, transparent: 0); } public static IPixelViewModel BuildInvisibleEventRender(IPixelViewModel colors) { var pixels = new short[colors.PixelData.Length]; for (int x = 0; x < colors.PixelWidth; x++) { for (int y = 0; y < colors.PixelHeight; y++) { if (((x + y) & 1) != 0) pixels[y * colors.PixelWidth + x] = colors.Transparent; else pixels[y * colors.PixelWidth + x] = colors.PixelData[y * colors.PixelWidth + x]; } } return new ReadonlyPixelViewModel(new SpriteFormat(4, colors.PixelWidth / 8, colors.PixelHeight / 8, default), pixels, colors.Transparent); } } public class ObjectEventViewModel : BaseEventViewModel { private readonly ScriptParser parser; private readonly EventTemplate eventTemplate; private readonly BerryInfo berries; private readonly Action gotoAddress; public event EventHandler DataMoved; public int Start => element.Start; public int ObjectID { get => element.GetValue("id"); set { element.SetValue("id", value); NotifyPropertyChanged(); } } public int Graphics { get => element.GetValue("graphics"); set { element.SetValue("graphics", value); RaiseEventVisualUpdated(); NotifyPropertyChanged(); } } /// /// FireRed Only. /// Kind is either 0 or 255. /// If it's 255, then this is an 'offscreen' object, which is a copy of an object in a connected map. /// The trainerType and trainerRangeOrBerryID have the map and bank information, respectively. /// public bool HasKind => element.HasField("kind"); public bool Kind { get => element.TryGetValue("kind", out int value) ? value != 0 : false; set { if (element.HasField("kind")) element.SetValue("kind", value ? 0xFF : 0); } } public int MoveType { get => element.GetValue("moveType"); set { element.SetValue("moveType", value); FacingOptions.Update(FacingOptions.AllOptions, MoveType); RaiseEventVisualUpdated(); NotifyPropertyChanged(); } } #region Range public int RangeX { get => element.GetValue("range") & 0xF; set { element.SetValue("range", (RangeY << 4) | value); rangeXY = null; NotifyPropertyChanged(nameof(RangeXY)); RaiseEventVisualUpdated(); } } public int RangeY { get => element.GetValue("range") >> 4; set { element.SetValue("range", (value << 4) | RangeX); rangeXY = null; NotifyPropertyChanged(nameof(RangeXY)); RaiseEventVisualUpdated(); } } private string rangeXY; public string RangeXY { get { if (rangeXY == null) rangeXY = $"({RangeX}, {RangeY})"; return rangeXY; } set { rangeXY = value; var parts = value.Split(new[] { ',', ' ', '(', ')' }, StringSplitOptions.RemoveEmptyEntries); if (parts.Length != 2) return; if (parts[0].TryParseInt(out int x) && parts[1].TryParseInt(out int y)) element.SetValue("range", (y << 4) | x); NotifyPropertyChanged(nameof(RangeX)); NotifyPropertyChanged(nameof(RangeY)); NotifyPropertyChanged(); RaiseEventVisualUpdated(); } } #endregion public int TrainerType { get => element.GetValue("trainerType"); set { element.SetValue("trainerType", value); NotifyPropertiesChanged(nameof(ShowBerryContent), nameof(ShowTrainerContent)); NotifyPropertyChanged(); } } public int TrainerRangeOrBerryID { get => element.GetValue("trainerRangeOrBerryID"); set { element.SetValue("trainerRangeOrBerryID", value); RaiseEventVisualUpdated(); NotifyPropertiesChanged(nameof(ShowBerryContent), nameof(BerryText)); NotifyPropertyChanged(); } } public int ScriptAddress { get => element.GetAddress("script"); set { element.SetAddress("script", value); NotifyPropertyChanged(); trainerSprite = null; scriptAddressText = npcText = martContentText = martHello = martGoodbye = trainerAfterText = trainerBeforeText = trainerWinText = tutorFailedText = tutorInfoText = tutorSuccessText = tutorWhichPokemonText = tradeFailedText = tradeInitialText = tradeSuccessText = tradeThanksText = null; NotifyPropertiesChanged( nameof(ScriptAddressText), nameof(ShowItemContents), nameof(ItemContents), nameof(ShowNpcText), nameof(NpcText), nameof(ShowTrainerContent), nameof(TrainerClass), nameof(TrainerSprite), nameof(TrainerName), nameof(TrainerBeforeText), nameof(TrainerAfterText), nameof(TrainerWinText), nameof(TrainerTeam), nameof(ShowMartContents), nameof(MartHello), nameof(MartContent), nameof(MartGoodbye), nameof(ShowTutorContent), nameof(TutorInfoText), nameof(TutorWhichPokemonText), nameof(TutorFailedText), nameof(TutorSucessText), nameof(TutorNumber), nameof(ShowTradeContent), nameof(TradeFailedText), nameof(TradeIndex), nameof(TradeInitialText), nameof(TradeSuccessText), nameof(TradeThanksText), nameof(TradeWrongSpeciesText), nameof(ShowBerryContent), nameof(BerryText), nameof(CanCreateScript)); } } public void GotoScript() => gotoAddress(ScriptAddress); public bool CanGotoScript => 0 <= ScriptAddress && ScriptAddress < element.Model.Count; public bool CanCreateScript => ScriptAddress == Pointer.NULL; public void CreateScript() { var start = element.Model.FindFreeSpace(element.Model.FreeSpaceStart, 0x10); Token.ChangeData(element.Model, start, 2); ScriptAddress = start; gotoAddress(start); } private string scriptAddressText; public string ScriptAddressText { get { if (scriptAddressText != null) return scriptAddressText; var value = element.GetAddress("script"); return GetAddressText(value, ref scriptAddressText); } set { SetAddressText(value, ref scriptAddressText, "script"); NotifyPropertyChanged(); NotifyPropertyChanged(nameof(ScriptAddress)); } } public int Flag { get => element.GetValue("flag"); set { element.SetValue("flag", value); NotifyPropertyChanged(); flagText = null; NotifyPropertyChanged(nameof(FlagText), nameof(SampleLegendClearScript)); } } string flagText; public string FlagText { get { if (flagText == null) flagText = element.GetValue("flag").ToString("X4"); return flagText; } set { flagText = value; element.SetValue("flag", value.TryParseHex(out int result) ? result : 0); NotifyPropertyChanged(); NotifyPropertyChanged(nameof(Flag), nameof(SampleLegendClearScript)); } } public int Padding { get => element.TryGetValue("padding", out var value) ? value : 0; set { element.SetValue("padding", value); NotifyPropertyChanged(); } } public IPixelViewModel DefaultOW { get; } public ObservableCollection Options { get; } = new(); public FilteringComboOptions FacingOptions { get; } = new(); public ObservableCollection ClassOptions { get; } = new(); public ObservableCollection ItemOptions { get; } = new(); #region Extended Properties // For certain simple events (npcs, trainers, items, signposts), // We can provide an enriched editing experience in the event panel. // These are the 'show' properties for those controls. public bool ShowItemContents => EventTemplate.GetItemAddress(element.Model, this) != Pointer.NULL; public int ItemContents { get { var itemAddress = EventTemplate.GetItemAddress(element.Model, this); if (itemAddress == Pointer.NULL) return -1; return element.Model.ReadMultiByteValue(itemAddress, 2); } set { var itemAddress = EventTemplate.GetItemAddress(element.Model, this); if (itemAddress == Pointer.NULL) return; element.Model.WriteMultiByteValue(itemAddress, 2, element.Token, value); NotifyPropertyChanged(); } } public bool ShowNpcText => EventTemplate.GetNPCTextPointer(element.Model, this) != Pointer.NULL; private string npcText; public string NpcText { get { if (npcText != null) return npcText; return npcText = GetText(EventTemplate.GetNPCTextPointer(element.Model, this)); } set { npcText = value; var newStart = SetText(EventTemplate.GetNPCTextPointer(element.Model, this), value); if (newStart != -1) DataMoved.Raise(this, new("Text", newStart)); } } #region Trainer Content public FilteringComboOptions TrainerOptions { get; } = new(); public bool ShowTrainerContent => EventTemplate.GetTrainerContent(element.Model, this) != null && TrainerType != 0; public int TrainerClass { get { var trainerContent = EventTemplate.GetTrainerContent(element.Model, this); if (trainerContent == null) return -1; return element.Model[trainerContent.TrainerClassAddress]; } set { var trainerContent = EventTemplate.GetTrainerContent(element.Model, this); if (trainerContent == null) return; element.Token.ChangeData(element.Model, trainerContent.TrainerClassAddress, (byte)value); var options = TrainerOptions.AllOptions.ToList(); options[trainerContent.TrainerIndex] = CreateOption(trainerContent.TrainerIndex, value, TrainerName); TrainerOptions.Update(options, trainerContent.TrainerIndex); } } private IPixelViewModel trainerSprite; public IPixelViewModel TrainerSprite { get { if (trainerSprite != null) return trainerSprite; var trainerContent = EventTemplate.GetTrainerContent(element.Model, this); if (trainerContent == null) return null; var spriteIndex = element.Model[trainerContent.TrainerClassAddress + 2]; var spriteAddress = element.Model.GetTableModel(HardcodeTablesModel.TrainerSpritesName)[spriteIndex].GetAddress("sprite"); var spriteRun = element.Model.GetNextRun(spriteAddress) as ISpriteRun; return trainerSprite = ReadonlyPixelViewModel.Create(element.Model, spriteRun, true, .5); } } private string trainerName; public string TrainerName { get { if (trainerName != null) return trainerName; var trainerContent = EventTemplate.GetTrainerContent(element.Model, this); if (trainerContent == null) return null; var text = element.Model.TextConverter.Convert(element.Model, trainerContent.TrainerNameAddress, 12); return trainerName = text.Trim('"'); } set { trainerName = value; var trainerContent = EventTemplate.GetTrainerContent(element.Model, this); if (trainerContent == null) return; var bytes = element.Model.TextConverter.Convert(value, out _); while (bytes.Count > 12) { bytes.RemoveAt(bytes.Count - 1); bytes[bytes.Count - 1] = 0xFF; } while (bytes.Count < 12) bytes.Add(0); element.Token.ChangeData(element.Model, trainerContent.TrainerNameAddress, bytes); NotifyPropertyChanged(); var options = TrainerOptions.AllOptions.ToList(); options[trainerContent.TrainerIndex] = CreateOption(trainerContent.TrainerIndex, element.Model[trainerContent.TrainerClassAddress], value); TrainerOptions.Update(options, trainerContent.TrainerIndex); } } public void RefreshTrainerOptions() { var trainerTable = element.Model.GetTableModel(HardcodeTablesModel.TrainerTableName); var trainers = element.Model.GetOptions(HardcodeTablesModel.TrainerTableName); if (trainerTable == null) return; var trainerContent = EventTemplate.GetTrainerContent(element.Model, this); var options = new List(); for (int i = 0; i < trainers.Count; i++) { options.Add(CreateOption(i, trainerTable[i].GetValue(1), trainers[i])); } TrainerOptions.Update(options, trainerContent?.TrainerIndex ?? 0); } private string trainerBeforeText; public string TrainerBeforeText { get { if (trainerBeforeText != null) return trainerBeforeText; var trainerContent = EventTemplate.GetTrainerContent(element.Model, this); if (trainerContent == null) return null; return trainerBeforeText = GetText(trainerContent.BeforeTextPointer); } set { trainerBeforeText = value; var trainerContent = EventTemplate.GetTrainerContent(element.Model, this); if (trainerContent == null) return; var newStart = SetText(trainerContent.BeforeTextPointer, value); if (newStart != -1) DataMoved.Raise(this, new("Text", newStart)); } } private string trainerWinText; public string TrainerWinText { get { if (trainerWinText != null) return trainerWinText; var trainerContent = EventTemplate.GetTrainerContent(element.Model, this); if (trainerContent == null) return null; return trainerWinText = GetText(trainerContent.WinTextPointer); } set { trainerWinText = value; var trainerContent = EventTemplate.GetTrainerContent(element.Model, this); if (trainerContent == null) return; var newStart = SetText(trainerContent.WinTextPointer, value); if (newStart != -1) DataMoved.Raise(this, new("Text", newStart)); } } private string trainerAfterText; public string TrainerAfterText { get { if (trainerAfterText != null) return trainerAfterText; var trainerContent = EventTemplate.GetTrainerContent(element.Model, this); if (trainerContent == null) return null; return trainerAfterText = GetText(trainerContent.AfterTextPointer); } set { trainerAfterText = value; var trainerContent = EventTemplate.GetTrainerContent(element.Model, this); if (trainerContent == null) return; var newStart = SetText(trainerContent.AfterTextPointer, value); if (newStart != -1) DataMoved.Raise(this, new("Text", newStart)); } } private string teamText; public string TrainerTeam { get { if (teamText != null) return teamText; var trainerContent = EventTemplate.GetTrainerContent(element.Model, this); if (trainerContent == null) return null; var address = element.Model.ReadPointer(trainerContent.TeamPointer); if (address < 0 || address >= element.Model.Count) return null; if (element.Model.GetNextRun(address) is not TrainerPokemonTeamRun run) return null; if (run.Start != address) return null; if (TeamVisualizations.Count == 0) UpdateTeamVisualizations(run); return teamText = run.SerializeRun(); } set { teamText = value; var trainerContent = EventTemplate.GetTrainerContent(element.Model, this); if (trainerContent == null) return; var address = element.Model.ReadPointer(trainerContent.TeamPointer); if (address < 0 || address >= element.Model.Count) return; if (element.Model.GetNextRun(address) is not TrainerPokemonTeamRun run) return; if (run.Start != address) return; var newRun = run.DeserializeRun(value, element.Token, false, false, out _); element.Model.ObserveRunWritten(element.Token, newRun); if (newRun.Start != run.Start) DataMoved.Raise(this, new("Trainer Team", newRun.Start)); UpdateTeamVisualizations(newRun); NotifyPropertyChanged(); } } public ObservableCollection TeamVisualizations { get; } = new(); private void UpdateTeamVisualizations(TrainerPokemonTeamRun team) { TeamVisualizations.Clear(); foreach (var vis in team.Visualizations) { TeamVisualizations.Add(vis); } } private StubCommand openTrainerData; public ICommand OpenTrainerData => StubCommand(ref openTrainerData, () => { var trainerContent = EventTemplate.GetTrainerContent(element.Model, this); if (trainerContent == null) return; gotoAddress(trainerContent.TrainerClassAddress - 1); }); public static ComboOption CreateOption(IReadOnlyList classOptions, int index, int trainerClass, string name) => new($"{index} - {classOptions[trainerClass.LimitToRange(0, classOptions.Count - 1)]} {name}", index); private ComboOption CreateOption(int index, int trainerClass, string name) => CreateOption(ClassOptions, index, trainerClass, name); #endregion #region Mart Content private Lazy martContent; public bool ShowMartContents => martContent.Value != null; private string martHello, martContentText, martGoodbye; public string MartHello { get => GetText(ref martHello, martContent.Value?.HelloPointer); set => SetText(ref martHello, martContent.Value?.HelloPointer, value, "Text"); } public string MartContent { get { if (martContentText != null) return martContentText; if (martContent.Value == null) return null; var martStart = element.Model.ReadPointer(martContent.Value.MartPointer); if (element.Model.GetNextRun(martStart) is not IStreamRun stream) return null; var lines = stream.SerializeRun().SplitLines().Select(line => line.Trim('"')); return martContentText = Environment.NewLine.Join(lines); } set { martContentText = value; if (martContent.Value == null) return; var martStart = element.Model.ReadPointer(martContent.Value.MartPointer); if (element.Model.GetNextRun(martStart) is not IStreamRun stream) return; var newStream = stream.DeserializeRun(value, Token, out var _, out var _); element.Model.ObserveRunWritten(Token, newStream); if (newStream.Start != stream.Start) DataMoved.Raise(this, new("Mart", newStream.Start)); } } public string MartGoodbye { get => GetText(ref martGoodbye, martContent.Value?.GoodbyePointer); set => SetText(ref martGoodbye, martContent.Value?.GoodbyePointer, value, "Text"); } #endregion #region Tutor Content private Lazy tutorContent; public bool ShowTutorContent { get { var content = tutorContent.Value; if (content != null && TutorOptions .AllOptions == null) { TutorOptions.Update(ComboOption.Convert(element.Model.GetOptions(HardcodeTablesModel.MoveTutors)), TutorNumber); TutorOptions.Bind(nameof(TutorOptions.SelectedIndex), (sender, e) => TutorNumber = TutorOptions.SelectedIndex); } return tutorContent.Value != null; } } private string tutorInfoText, tutorWhichPokemonText, tutorFailedText, tutorSuccessText; public string TutorInfoText { get => GetText(ref tutorInfoText, tutorContent.Value?.InfoPointer); set => SetText(ref tutorInfoText, tutorContent.Value?.InfoPointer, value, "Text"); } public string TutorWhichPokemonText { get => GetText(ref tutorWhichPokemonText, tutorContent.Value?.WhichPokemonPointer); set => SetText(ref tutorWhichPokemonText, tutorContent.Value?.WhichPokemonPointer, value, "Text"); } public string TutorFailedText { get => GetText(ref tutorFailedText, tutorContent.Value?.FailedPointer); set => SetText(ref tutorFailedText, tutorContent.Value?.FailedPointer, value, "Text"); } public string TutorSucessText { get => GetText(ref tutorSuccessText, tutorContent.Value?.SuccessPointer); set => SetText(ref tutorSuccessText, tutorContent.Value?.SuccessPointer, value, "Text"); } public int TutorNumber { get { if (tutorContent.Value == null) return -1; return element.Model.ReadMultiByteValue(tutorContent.Value.TutorAddress, 2); } set { if (tutorContent.Value == null) return; element.Model.WriteMultiByteValue(tutorContent.Value.TutorAddress, 2, Token, value); } } public FilteringComboOptions TutorOptions { get; } = new(); public void GotoTutors() => gotoAddress(element.Model.GetTableModel(HardcodeTablesModel.MoveTutors)[TutorNumber].Start); #endregion #region Trade Content private Lazy tradeContent; public FilteringComboOptions TradeOptions { get; } = new(); public bool ShowTradeContent { get { var content = tradeContent.Value; if (content != null && TradeOptions.AllOptions == null) { var pokenames = element.Model.GetOptions(HardcodeTablesModel.PokemonNameTable); var options = new List(); foreach (var trade in element.Model.GetTableModel(HardcodeTablesModel.TradeTable)) { if (!trade.TryGetValue("receive", out int receive) || !trade.TryGetValue("give", out int give)) { options.Add(options.Count.ToString()); } else { options.Add($"{pokenames[give]} -> {pokenames[receive]}"); } } TradeOptions.Update(ComboOption.Convert(options), TradeIndex); TradeOptions.Bind(nameof(TradeOptions.SelectedIndex), (sender, e) => TradeIndex = TradeOptions.SelectedIndex); } return tradeContent.Value != null; } } private string tradeInitialText, tradeThanksText, tradeSuccessText, tradeFailedText, tradeWrongSpeciesText; public string TradeInitialText { get => GetText(ref tradeInitialText, tradeContent.Value?.InfoPointer); set => SetText(ref tradeInitialText, tradeContent.Value?.InfoPointer, value, "Text"); } public string TradeThanksText { get => GetText(ref tradeThanksText, tradeContent.Value?.ThanksPointer); set => SetText(ref tradeThanksText, tradeContent.Value?.ThanksPointer, value, "Text"); } public string TradeSuccessText { get => GetText(ref tradeSuccessText, tradeContent.Value?.SuccessPointer); set => SetText(ref tradeSuccessText, tradeContent.Value?.SuccessPointer, value, "Text"); } public string TradeFailedText { get => GetText(ref tradeFailedText, tradeContent.Value?.FailedPointer); set => SetText(ref tradeFailedText, tradeContent.Value?.FailedPointer, value, "Text"); } public string TradeWrongSpeciesText { get => GetText(ref tradeWrongSpeciesText, tradeContent.Value?.WrongSpeciesPointer); set => SetText(ref tradeWrongSpeciesText, tradeContent.Value?.WrongSpeciesPointer, value, "Text"); } public int TradeIndex { get { if (tradeContent.Value == null) return -1; return element.Model.ReadMultiByteValue(tradeContent.Value.TradeAddress, 2); } set { if (tradeContent.Value == null) return; element.Model.WriteMultiByteValue(tradeContent.Value.TradeAddress, 2, Token, value); } } public void GotoTrades() => gotoAddress(element.Model.GetTableModel(HardcodeTablesModel.TradeTable)[TradeIndex].Start); #endregion #region Legendary Content private Lazy legendaryContent; public bool ShowLegendaryContent { get { var content = legendaryContent.Value; if (content != null && PokemonOptions.AllOptions == null) { var options = ComboOption.Convert(element.Model.GetOptions(HardcodeTablesModel.PokemonNameTable)); PokemonOptions.Update(options, element.Model.ReadMultiByteValue(content.SetWildBattle + 1, 2)); PokemonOptions.Bind(nameof(PokemonOptions.SelectedIndex), (sender, e) => { element.Model.WriteMultiByteValue(content.Cry + 1, 2, element.Token, PokemonOptions.SelectedIndex); element.Model.WriteMultiByteValue(content.SetWildBattle + 1, 2, element.Token, PokemonOptions.SelectedIndex); foreach (var buffer in content.BufferPokemon) element.Model.WriteMultiByteValue(buffer + 2, 2, element.Token, PokemonOptions.SelectedIndex); }); } if (content != null && HoldItemOptions.AllOptions == null) { var options = ComboOption.Convert(element.Model.GetOptions(HardcodeTablesModel.ItemsTableName)); HoldItemOptions.Update(options, element.Model.ReadMultiByteValue(content.SetWildBattle + 4, 2)); HoldItemOptions.Bind(nameof(HoldItemOptions.SelectedIndex), (sender, e) => element.Model.WriteMultiByteValue(content.SetWildBattle + 4, 2, element.Token, HoldItemOptions.SelectedIndex)); } return content != null; } } public FilteringComboOptions PokemonOptions { get; } = new(); public void GotoPokemon() => gotoAddress(element.Model.GetTableModel(HardcodeTablesModel.PokemonNameTable)[PokemonOptions.SelectedIndex].Start); public int Level { get => legendaryContent.Value == null ? -1 : element.Model[legendaryContent.Value.SetWildBattle + 3]; set { if (legendaryContent.Value == null) return; element.Token.ChangeData(element.Model, legendaryContent.Value.SetWildBattle + 3, (byte)value); } } public FilteringComboOptions HoldItemOptions { get; } = new(); public void GotoHoldItem() => gotoAddress(element.Model.GetTableModel(HardcodeTablesModel.ItemsTableName)[HoldItemOptions.SelectedIndex].Start); private string legendaryFlagText; public string LegendaryFlagText { get { if (legendaryContent.Value == null) return null; if (legendaryFlagText == null) legendaryFlagText = element.Model.ReadMultiByteValue(legendaryContent.Value.SetFlag[0] + 1, 2).ToString("X4"); return legendaryFlagText; } set { if (legendaryContent.Value == null) return; legendaryFlagText = value; foreach (var flag in legendaryContent.Value.SetFlag) { element.Model.WriteMultiByteValue(flag + 1, 2, element.Token, value.TryParseHex(out int result) ? result : 0); } NotifyPropertyChanged(); NotifyPropertyChanged(nameof(SampleLegendClearScript)); } } public bool HasCryText => (legendaryContent.Value?.CryTextPointer ?? Pointer.NULL) != Pointer.NULL; private string cryText; public string CryText { get => GetText(ref cryText, legendaryContent.Value?.CryTextPointer); set => SetText(ref cryText, legendaryContent.Value?.CryTextPointer, value, "Cry"); } private TextEditorViewModel sampleLegendClearScript; public TextEditorViewModel SampleLegendClearScript { get { var script = @$" # whatever if.flag.clear.call 0x{LegendaryFlagText} # whatever end show: clearflag 0x{FlagText} return"; if (sampleLegendClearScript == null) { sampleLegendClearScript = new TextEditorViewModel() { LineCommentHeader = "#" }; sampleLegendClearScript.Keywords.Add("if.flag.clear.call"); sampleLegendClearScript.Keywords.Add("end"); sampleLegendClearScript.Keywords.Add("clearflag"); sampleLegendClearScript.Keywords.Add("return"); } sampleLegendClearScript.Content = script; return sampleLegendClearScript; } } #endregion #region Berry Content public bool ShowBerryContent => TrainerType == 0 && TrainerRangeOrBerryID != 0; public string BerryText { get { if (berries.BerryMap.TryGetValue(TrainerRangeOrBerryID, out BerrySpot spot)) { if (spot.BerryID >= 0 && spot.BerryID < berries.BerryOptions.Count) { return berries.BerryOptions[spot.BerryID]; } } return "Unknown"; } } public void GotoBerryCode() { if (berries.BerryMap.TryGetValue(TrainerRangeOrBerryID, out BerrySpot spot)) { gotoAddress(spot.Address); } } #endregion private string GetText(ref string cache, int? pointer) { if (cache != null) return cache; if (pointer == null) return null; return cache = GetText((int)pointer); } private void SetText(ref string cache, int? pointer, string value, string type, [CallerMemberName] string propertyName = null) { cache = value; if (pointer == null) return; var newStart = SetText((int)pointer, value, propertyName); if (newStart != -1) DataMoved.Raise(this, new(type, newStart)); } #endregion public ObjectEventViewModel(ScriptParser parser, Action gotoAddress, ModelArrayElement objectEvent, EventTemplate eventTemplate, IReadOnlyList sprites, IPixelViewModel defaultSprite, BerryInfo berries) : base(objectEvent, "objectCount") { this.parser = parser; this.gotoAddress = gotoAddress; this.eventTemplate = eventTemplate; this.berries = berries; for (int i = 0; i < sprites.Count; i++) Options.Add(VisualComboOption.CreateFromSprite(i.ToString(), sprites[i].PixelData, sprites[i].PixelWidth, i, 2, true)); DefaultOW = defaultSprite; objectEvent.Model.TryGetList("FacingOptions", out var list); FacingOptions.Update(ComboOption.Convert(list), MoveType); FacingOptions.Bind(nameof(FacingOptions.SelectedIndex), (sender, e) => MoveType = FacingOptions.SelectedIndex); foreach (var item in objectEvent.Model.GetOptions(HardcodeTablesModel.TrainerClassNamesTable)) ClassOptions.Add(item); foreach (var item in objectEvent.Model.GetOptions(HardcodeTablesModel.ItemsTableName)) ItemOptions.Add(item); RefreshTrainerOptions(); TrainerOptions.Bind(nameof(TrainerOptions.SelectedIndex), (options, args) => { this.eventTemplate.UseTrainerFlag(TrainerOptions.SelectedIndex); var trainerContent = EventTemplate.GetTrainerContent(element.Model, this); element.Model.WriteMultiByteValue(trainerContent.TrainerIndexAddress, 2, () => element.Token, TrainerOptions.SelectedIndex); TeamVisualizations.Clear(); trainerSprite = null; trainerName = trainerBeforeText = trainerWinText = trainerAfterText = teamText = null; NotifyPropertiesChanged(nameof(TrainerSprite), nameof(TrainerName), nameof(TrainerBeforeText), nameof(TrainerWinText), nameof(TrainerAfterText), nameof(TrainerTeam), nameof(TrainerClass)); }); tutorContent = new Lazy(() => EventTemplate.GetTutorContent(element.Model, parser, this)); martContent = new Lazy(() => EventTemplate.GetMartContent(element.Model, parser, this)); tradeContent = new Lazy(() => EventTemplate.GetTradeContent(element.Model, parser, this)); legendaryContent = new Lazy(() => EventTemplate.GetLegendaryEventContent(element.Model, parser, this)); } public override int TopOffset => 16 - (EventRender?.PixelHeight ?? 0); public override int LeftOffset => (16 - (EventRender?.PixelWidth ?? 0)) / 2; public override void Render(IDataModel model, LayoutModel layout) { var ows = model.GetTable(HardcodeTablesModel.OverworldSprites); var owTable = ows == null ? null : new ModelTable(model, ows.Start); var facing = MoveType switch { 7 => 1, 9 => 2, 10 => 3, 76 => 76, // invisible _ => 0, }; EventRender = Render(model, owTable, DefaultOW, Graphics, facing); NotifyPropertyChanged(nameof(EventRender)); } /// (0, 1, 2, 3) = (down, up, left, right) public static IPixelViewModel Render(IDataModel model, ModelTable owTable, IPixelViewModel defaultOW, int index, int facing) { if (owTable == null || index >= owTable.Count) return defaultOW; var element = owTable[index]; var data = element.GetSubTable("data")[0]; var sprites = data.GetSubTable("sprites"); if (sprites == null) return defaultOW; bool invisible = facing == 76; bool flip = facing == 3; if (facing == 3) facing = 2; if (facing >= sprites.Count) facing = 0; var graphicsAddress = sprites.Run.Start; var pointerAddress = data.Start; var graphicsRun = model.GetNextRun(graphicsAddress) as ISpriteRun; var paletteRun = graphicsRun.FindRelatedPalettes(model, pointerAddress).FirstOrDefault(); if (facing != -1) { var sprite = sprites[facing]; graphicsAddress = sprite.GetAddress("sprite"); graphicsRun = model.GetNextRun(graphicsAddress) as ISpriteRun; } if (graphicsRun == null) return defaultOW; if (paletteRun == null) return defaultOW; var ow = ReadonlyPixelViewModel.Create(model, graphicsRun, paletteRun, true); if (invisible) ow = BuildInvisibleEventRender(ow); if (flip) ow = ow.ReflectX(); return ow; } public void ClearUnused() { element.SetValue(2, 0); element.SetValue(12, 0); } private static readonly Dictionary facingVectors = new() { [7] = new(0, -1), [8] = new(0, 1), [9] = new(-1, 0), [10] = new(1, 0), }; public bool ShouldHighlight(int x, int y) { if (TrainerType != 0 && facingVectors.TryGetValue(MoveType, out var vector)) { var (xx, yy) = (X, Y); var range = TrainerRangeOrBerryID; if (Math.Sign(y - yy) == vector.Y && Math.Sign(x - xx) == vector.X && Math.Abs(y - yy) <= range && Math.Abs(x - xx) <= range) { return true; } } else { if (!MoveType.IsAny(2, 3, 4, 5, 6)) return false; if (Math.Abs(x - X) <= RangeX && Math.Abs(y - Y) <= RangeY) return true; } return false; } } public class WarpEventViewModel : BaseEventViewModel { public WarpEventViewModel(ModelArrayElement warpEvent) : base(warpEvent, "warpCount") { } public int WarpID { get => element.GetValue("warpID") + 1; set => element.SetValue("warpID", value - 1); } #region Bank/Map public int Bank { get => element.GetValue("bank"); set { element.SetValue("bank", value); NotifyPropertyChanged(); if (!ignoreUpdateBankMap) bankMap = null; NotifyPropertyChanged(nameof(BankMap)); NotifyPropertyChanged(nameof(TargetMapName)); } } public int Map { get => element.GetValue("map"); set { element.SetValue("map", value); NotifyPropertyChanged(); if (!ignoreUpdateBankMap) bankMap = null; NotifyPropertyChanged(nameof(BankMap)); NotifyPropertyChanged(nameof(TargetMapName)); } } private bool ignoreUpdateBankMap; private string bankMap; public string BankMap { get { if (bankMap == null) bankMap = $"({Bank}, {Map})"; return bankMap; } set { bankMap = value; var parts = value.Split(new[] { ',', ' ', '(', ')' }, StringSplitOptions.RemoveEmptyEntries); if (parts.Length != 2) return; ignoreUpdateBankMap = true; if (parts[0].TryParseInt(out int bank)) Bank = bank; if (parts[1].TryParseInt(out int map)) Map = map; ignoreUpdateBankMap = false; } } public string TargetMapName => BlockMapViewModel.MapIDToText(element.Model, Bank, Map); public WarpEventModel WarpModel => new WarpEventModel(element); #endregion public override void Render(IDataModel model, LayoutModel layout) { EventRender = BuildEventRender(UncompressedPaletteColor.Pack(0, 0, 31)); if (WarpIsOnWarpableBlock(model, layout)) return; EventRender = BuildEventRender(UncompressedPaletteColor.Pack(0, 0, 31), true); } public bool WarpIsOnWarpableBlock(IDataModel model, LayoutModel layout) { if (!model.TryGetList("MapAttributeBehaviors", out var list)) return false; int primaryBlockCount = model.IsFRLG() ? 640 : 512; var cell = layout.BlockMap[X, Y]; var tile = cell.Tile; var blockset = layout.PrimaryBlockset; if (tile >= primaryBlockCount) { tile -= primaryBlockCount; blockset = layout.SecondaryBlockset; } var behavior = blockset.Attribute(tile).Behavior; if (list.Count <= behavior) return false; return new[] { "Warp", "Door", "Stairs", "Ladder", "Escalator" }.Any(list[behavior].Contains); } } public class ScriptEventViewModel : BaseEventViewModel { private readonly Action gotoAddress; public ScriptEventViewModel(Action gotoAddress, ModelArrayElement scriptEvent) : base(scriptEvent, "scriptCount") { this.gotoAddress = gotoAddress; } public int Trigger { get => element.GetValue("trigger"); set => element.SetValue("trigger", value); } private string triggerHex; public string TriggerHex { get { if (triggerHex != null) return triggerHex; return triggerHex = Trigger.ToString("X4"); } set { triggerHex = value; if (!value.TryParseHex(out int result)) return; Trigger = result; } } public int Index { get => element.GetValue("index"); set => element.SetValue("index", value); } public int ScriptAddress { get => element.GetAddress("script"); set { element.SetAddress("script", value); NotifyPropertyChanged(nameof(CanCreateScript)); } } public void GotoScript() => gotoAddress(ScriptAddress); public bool CanCreateScript => ScriptAddress == Pointer.NULL; public void CreateScript() { var start = element.Model.FindFreeSpace(element.Model.FreeSpaceStart, 0x10); Token.ChangeData(element.Model, start, 2); ScriptAddress = start; gotoAddress(start); } private string scriptAddressText; public string ScriptAddressText { get { if (scriptAddressText != null) return scriptAddressText; var value = element.GetAddress("script"); return GetAddressText(value, ref scriptAddressText); } set { SetAddressText(value, ref scriptAddressText, "script"); NotifyPropertyChanged(); NotifyPropertyChanged(nameof(ScriptAddress)); } } public override void Render(IDataModel model, LayoutModel layout) { EventRender = BuildEventRender(UncompressedPaletteColor.Pack(0, 31, 0)); } } public class SignpostEventViewModel : BaseEventViewModel { // kind. arg::|h // kind = 0/1/2/3/4 => arg is a pointer to an XSE script // kind = 5/6/7 => arg is itemID: hiddenItemID. attr|t|quantity:::.|isUnderFoot. // kind = 8 => arg is secret base ID, just a 4-byte hex number // hidden item IDs are just flags starting at 0x3E8 (1000). private readonly Action gotoAddress; public event EventHandler DataMoved; public SignpostEventViewModel(ModelArrayElement signpostEvent, Action gotoAddress) : base(signpostEvent, "signpostCount") { if (signpostEvent.Model.TryGetList("MapSignpostKindOptions", out var names)) names.ForEach(KindOptions.Add); foreach (var item in signpostEvent.Model.GetOptions(HardcodeTablesModel.ItemsTableName)) { ItemOptions.Add(item); } SetDestinationFormat(); this.gotoAddress = gotoAddress; } public void SetDestinationFormat() { if (!ShowPointer) return; var destinationRun = new XSERun(Pointer, SortedSpan.None); var existingRun = element.Model.GetNextRun(destinationRun.Start); if (existingRun.Start < destinationRun.Start) return; // don't erase existing runs for this if (existingRun.Start == destinationRun.Start && existingRun is not NoInfoRun) return; element.Model.ObserveRunWritten(new ModelDelta(), destinationRun); // don't track this change } public void ClearDestinationFormat() { if (!ShowPointer) return; var destination = Pointer; var run = element.Model.GetNextRun(Pointer); if (run.Start != destination || (run.PointerSources != null && run.PointerSources.Count > 0)) return; element.Model.ClearFormat(element.Token, destination, 1); } public ObservableCollection KindOptions { get; } = new(); public int Kind { get => element.TryGetValue("kind", out var value) ? value : -1; set { ClearDestinationFormat(); var old = element.GetValue("kind"); element.SetValue("kind", value); var wasPointer = old < 5; var isPointer = value < 5; NotifyPropertiesChanged(nameof(ShowArg), nameof(ShowPointer), nameof(ShowHiddenItemProperties)); if (ShowHiddenItemProperties) NotifyPropertyChanged(nameof(ItemID)); SetDestinationFormat(); if (wasPointer == isPointer) return; element.SetValue("arg", 0); argText = null; pointerText = null; NotifyPropertiesChanged(nameof(ArgText), nameof(PointerText), nameof(ShowSignpostText), nameof(ItemID), nameof(HiddenItemID), nameof(Quantity), nameof(CanGotoScript)); } } public bool ShowArg => Kind == 8; string argText; public string ArgText { get { if (argText != null) return argText; argText = element.GetValue("arg").ToString("X8"); return argText; } set { argText = value; if (value.TryParseHex(out int result)) element.SetValue("arg", result); } } #region Show as Pointer public bool ShowPointer => Kind < 5; public int Pointer { get => element.GetAddress("arg"); set { ClearDestinationFormat(); element.SetAddress("arg", value); pointerText = argText = null; NotifyPropertiesChanged(nameof(PointerText), nameof(ArgText), nameof(CanGotoScript)); SetDestinationFormat(); } } private string pointerText; public string PointerText { get { if (pointerText != null) return pointerText; var value = element.GetAddress("arg"); return GetAddressText(value, ref pointerText); } set { ClearDestinationFormat(); SetAddressText(value, ref pointerText, "arg"); SetDestinationFormat(); NotifyPropertyChanged(nameof(PointerText), nameof(CanGotoScript)); } } public bool CanGotoScript => 0 <= Pointer && Pointer < element.Model.Count; public void GotoScript() { SetDestinationFormat(); gotoAddress(Pointer); } #endregion #region Item Properties public bool ShowHiddenItemProperties => Kind >= 5 && Kind <= 7; // itemID: hiddenItemID. attr|t|quantity:::.|isUnderFoot. // arg is at offset '8' of the element public ObservableCollection ItemOptions { get; } = new(); public int ItemID { get => element.Model.ReadMultiByteValue(element.Start + 8, 2); set { element.Model.WriteMultiByteValue(element.Start + 8, 2, element.Token, value); NotifyPropertyChanged(nameof(ItemID)); } } public byte HiddenItemID { get => element.Model[element.Start + 10]; set => element.Token.ChangeData(element.Model, element.Start + 10, value); } public byte Quantity { get => (byte)(element.Model[element.Start + 11] & 0x7F); set { var newValue = (byte)((int)value).LimitToRange(0, 0x7F); var previous = element.Model[element.Start + 11]; newValue |= (byte)(previous & 0x80); Token.ChangeData(element.Model, element.Start + 11, newValue); NotifyPropertyChanged(nameof(Quantity)); } } public bool IsUnderFoot { get => element.Model[element.Start + 11] >= 0x80; set { byte newValue = value ? (byte)0x80 : (byte)0; var previous = element.Model[element.Start + 11]; newValue |= (byte)(previous & 0x7F); Token.ChangeData(element.Model, element.Start + 11, newValue); NotifyPropertyChanged(nameof(IsUnderFoot)); } } #endregion public override void Render(IDataModel model, LayoutModel layout) { EventRender = BuildEventRender(UncompressedPaletteColor.Pack(31, 0, 0)); } public bool ShowSignpostText => EventTemplate.GetSignpostTextPointer(element.Model, this) != DataFormats.Pointer.NULL; private string signpostText; public string SignpostText { get { if (signpostText != null) return signpostText; signpostText = GetText(EventTemplate.GetSignpostTextPointer(element.Model, this)); return signpostText; } set { signpostText = value; var newAddress = SetText(EventTemplate.GetSignpostTextPointer(element.Model, this), value); if (newAddress >= 0) DataMoved.Raise(this, new("Text", newAddress)); } } } }