mirror of
https://github.com/haven1433/HexManiacAdvance.git
synced 2026-06-01 13:03:39 -05:00
1493 lines
61 KiB
C#
1493 lines
61 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.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<IEventViewModel>, INotifyPropertyChanged {
|
|
event EventHandler EventVisualUpdated;
|
|
public event EventHandler<EventCycleDirection> 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<EventCycleDirection>(ref cycleEventCommand, direction => {
|
|
CycleEvent.Raise(this, direction);
|
|
});
|
|
|
|
public event EventHandler EventVisualUpdated;
|
|
public event EventHandler<EventCycleDirection> CycleEvent;
|
|
|
|
public static IEnumerable<FlyEventViewModel> Create(IDataModel model, int bank, int map, Func<ModelDelta> 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;
|
|
}
|
|
}
|
|
|
|
/// <returns>true if the event was deleted</returns>
|
|
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<IEventViewModel> {
|
|
public event EventHandler EventVisualUpdated;
|
|
public event EventHandler<EventCycleDirection> CycleEvent;
|
|
|
|
private StubCommand cycleEventCommand;
|
|
public ICommand CycleEventCommand => StubCommand<EventCycleDirection>(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 = "<null>";
|
|
}
|
|
return field;
|
|
}
|
|
|
|
protected void SetAddressText(string value, ref string field, string fieldName) {
|
|
field = value;
|
|
value = field.Trim("<nul> ".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<int> gotoAddress;
|
|
|
|
public event EventHandler<DataMovedEventArgs> 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();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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<VisualComboOption> Options { get; } = new();
|
|
public FilteringComboOptions FacingOptions { get; } = new();
|
|
public ObservableCollection<string> ClassOptions { get; } = new();
|
|
public ObservableCollection<string> 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<ComboOption>();
|
|
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<IPixelViewModel> 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<string> 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<MartEventContent> 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<TutorEventContent> 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<TradeEventContent> 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<string>();
|
|
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<LegendaryEventContent> 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} <show>
|
|
# 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<int> gotoAddress, ModelArrayElement objectEvent, EventTemplate eventTemplate, IReadOnlyList<IPixelViewModel> 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<TutorEventContent>(() => EventTemplate.GetTutorContent(element.Model, parser, this));
|
|
martContent = new Lazy<MartEventContent>(() => EventTemplate.GetMartContent(element.Model, parser, this));
|
|
tradeContent = new Lazy<TradeEventContent>(() => EventTemplate.GetTradeContent(element.Model, parser, this));
|
|
legendaryContent = new Lazy<LegendaryEventContent>(() => 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));
|
|
}
|
|
|
|
/// <param name="facing">(0, 1, 2, 3) = (down, up, left, right)</param>
|
|
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<int, Point> 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<int> gotoAddress;
|
|
|
|
public ScriptEventViewModel(Action<int> 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<int> gotoAddress;
|
|
|
|
public event EventHandler<DataMovedEventArgs> DataMoved;
|
|
|
|
public SignpostEventViewModel(ModelArrayElement signpostEvent, Action<int> 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<int>.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<string> 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<string> 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));
|
|
}
|
|
}
|
|
}
|
|
}
|