HexManiacAdvance/src/HexManiac.Core/ViewModels/Map/MapScriptCollection.cs
haven1433 a371b65a15 fix bugs
* don't freeze when AMap saves
* allow edit->paste to target the ViewPort even if it doesn't have focus
* notify on repoint
* command to jump to data in the viewport
* show hover point in the corner
* finish script UI (add, remove, edit)
* find record fields when doing Show Uses
* tooltip for address for rom overview
2022-10-19 20:00:24 -05:00

354 lines
15 KiB
C#

using HavenSoft.HexManiac.Core.Models;
using HavenSoft.HexManiac.Core.Models.Runs;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
namespace HavenSoft.HexManiac.Core.ViewModels.Map {
public record NewMapScriptsCreatedEventArgs(int Address);
public class MapScriptCollection : ViewModelCore {
private readonly IEditableViewPort viewPort;
private ModelArrayElement owner;
private int address;
public int Address => address;
public bool Unloaded => address == 0;
public ObservableCollection<MapScriptViewModel> Scripts { get; } = new();
public bool CollectionExists => address > 0;
public event EventHandler<NewMapScriptsCreatedEventArgs> NewMapScriptsCreated;
public MapScriptCollection(IEditableViewPort viewPort) => this.viewPort = viewPort;
public void Load(ModelArrayElement owner) {
Scripts.Clear();
this.owner = owner;
address = owner.GetAddress("mapscripts");
var model = viewPort.Model;
var scriptStart = address;
while (model[scriptStart] != 0) {
Scripts.Add(new(viewPort, scriptStart));
AddDeleteHandler(Scripts.Count - 1);
scriptStart += 5;
}
NotifyPropertiesChanged(nameof(CollectionExists), nameof(Unloaded), nameof(Address));
}
public bool CanCreateCollection => address < 0;
public void CreateCollection() {
var model = viewPort.Model;
var token = viewPort.ChangeHistory.CurrentChange;
var start = model.FindFreeSpace(model.FreeSpaceStart, 1);
token.ChangeData(model, start, 0x00);
address = start;
owner.SetAddress("mapscripts", address);
Scripts.Clear();
NotifyPropertiesChanged(nameof(Unloaded), nameof(CollectionExists), nameof(Address));
}
public IReadOnlyList<int> GetScripts() {
var results = new List<int>();
foreach (var child in Scripts) results.AddRange(child.GetScripts());
return results;
}
public void AddScript() {
var token = viewPort.ChangeHistory.CurrentChange;
var model = viewPort.Model;
var run = model.GetNextRun(address) as ITableRun;
if (run == null) return;
run = model.RelocateForExpansion(token, run, run.Length + 5);
run = run.Append(token, 1);
address = run.Start;
var newScript = model.FindFreeSpace(model.FreeSpaceStart, 1);
token.ChangeData(model, newScript, 0x02); // `end`
token.ChangeData(model, run.Start + run.Length - 6, 1); // script-type 1
model.UpdateArrayPointer(token, default, default, default, run.Start + run.Length - 5, newScript);
model.ObserveRunWritten(token, run);
model.ObserveRunWritten(token, new XSERun(newScript));
Scripts.Add(new(viewPort, address + Scripts.Count * 5));
AddDeleteHandler(Scripts.Count - 1);
}
private void AddDeleteHandler(int index) {
Scripts[index].DeleteMe += HandleDelete;
}
private void HandleDelete(object sender, MapScriptDeleteEventArgs e) {
var model = viewPort.Model;
var token = viewPort.ChangeHistory.CurrentChange;
var script = (MapScriptViewModel)sender;
var index = Scripts.IndexOf(script);
var tableRun = model.GetNextRun(address) as ITableRun;
if (tableRun == null) return;
var table = new ModelTable(model, tableRun, () => token); // type. pointer<>
for (int i = index; i < Scripts.Count - 1; i++) {
table[i].SetValue(0, table[i + 1].GetValue(0));
table[i].SetAddress("pointer", table[i + 1].GetAddress("pointer"));
}
tableRun = tableRun.Append(token, -1);
model.ObserveRunWritten(token, tableRun);
Scripts[index].DeleteMe -= HandleDelete;
Scripts.RemoveAt(index);
e.Success = true;
}
}
public class MapScriptDeleteEventArgs : EventArgs {
public bool Success { get; set; }
}
public class MapScriptViewModel : ViewModelCore {
private readonly IEditableViewPort viewPort;
private readonly int start;
private int scriptType, address;
private string displayAddress;
public event EventHandler<MapScriptDeleteEventArgs> DeleteMe;
public bool HasSubScripts => scriptType == 2 || scriptType == 4;
public ObservableCollection<VisualOption> ScriptOptions { get; } = new();
public ObservableCollection<MapSubScriptViewModel> SubScripts { get; } = new();
public MapScriptViewModel(IEditableViewPort viewPort, int start) {
this.viewPort = viewPort;
var model = viewPort.Model;
this.start = start;
this.scriptType = model[start];
this.address = model.ReadPointer(start + 1);
this.displayAddress = $"<{address:X6}>";
Load();
ScriptOptions.Add(new VisualOption {
Option = "Load",
ShortDescription = "Before layout is drawn",
Description = "Almost exclusively used to set metatiles on the map before it's first drawn",
});
ScriptOptions.Add(new VisualOption {
Option = "Per-Frame (Table)",
ShortDescription = "Run every frame",
Description = "Only the first script whose condition is satisfied is run. Used to trigger events.",
});
ScriptOptions.Add(new VisualOption {
Option = "Transition",
ShortDescription = "Run when switching maps",
Description = "Used to set map-specific flags/vars, update object positions/movement types, set weather, etc",
});
ScriptOptions.Add(new VisualOption {
Option = "Warp into Map (Table)",
ShortDescription = "Run after objects are loaded",
Description = "Only the first script whose condition is satisfied is run. Used to update facing / visibility or to add objects to the scene.",
});
ScriptOptions.Add(new VisualOption {
Option = "Resume",
ShortDescription = "Run after loading the map, or exiting the bag, or finishing a battle, etc",
Description = "Used to hide defeated static pokemon, or maintain some map state",
});
ScriptOptions.Add(new VisualOption {
Option = "Dive Warp",
ShortDescription = "Run after the player dives or emerges",
Description = "Only used to determine whether the player should emerge in the sealed chamber.",
});
ScriptOptions.Add(new VisualOption {
Option = "Return to Field",
ShortDescription = "Run after exiting bag or finishing battle, but not when entering a map",
Description = "Used rarely, when something must only happen on reload.",
});
}
private void Load() {
var model = viewPort.Model;
SubScripts.Clear();
if (scriptType != 2 && scriptType != 4) {
// we're pointing at an XSERun
if (model.GetNextRun(address).Start >= address) {
model.ObserveRunWritten(viewPort.ChangeHistory.CurrentChange, new XSERun(address));
}
} else {
var destination = address;
while (true) {
var currentValue = model.ReadMultiByteValue(destination, 2);
if (currentValue == 0 || currentValue == 0xFFFF) break;
var child = new MapSubScriptViewModel(viewPort, destination);
child.DeleteMe += HandleDelete;
SubScripts.Add(child);
destination += 8;
}
if (!ArrayRun.TryParse(model, "[variable:|h value: pointer<`xse`>]!0000", address, SortedSpan.One(start + 1), out var run).HasError) {
model.ClearFormat(viewPort.ChangeHistory.CurrentChange, run.Start, run.Length);
model.ObserveRunWritten(viewPort.ChangeHistory.CurrentChange, run);
}
}
}
public int ScriptTypeIndex {
get => scriptType - 1; set => Set(ref scriptType, value + 1, arg => {
NotifyPropertyChanged(nameof(HasSubScripts));
var model = viewPort.Model;
var token = viewPort.ChangeHistory.CurrentChange;
if ((arg == 2 || arg == 4) && (scriptType != 2 && scriptType != 4)) {
// if the old type is 2 or 4 and the new type is not, delete the content and replace it with a new 1-byte script `end` (02)
int destination;
if (SubScripts.Count == 0) {
destination = model.FindFreeSpace(model.FreeSpaceStart, 2);
token.ChangeData(model, destination, 0x02);
model.ObserveRunWritten(token, new XSERun(destination));
} else {
destination = model.ReadPointer(SubScripts[0].Start + 4);
}
model.UpdateArrayPointer(token, default, default, -1, this.start + 1, destination);
address = destination;
this.displayAddress = $"<{address:X6}>";
NotifyPropertyChanged(nameof(Address));
Load();
} else if ((scriptType == 2 || scriptType == 4) && (arg != 2 && arg != 4)) {
// if the new type is 2 or 4 and the old type is not, move the current script to be the first SubScript of the new table
var destination = model.FindFreeSpace(model.FreeSpaceStart, 10);
model.WriteMultiByteValue(destination + 0, 2, token, 1);
model.WriteMultiByteValue(destination + 2, 2, token, 1);
model.WritePointer(token, destination + 4, address);
model.WriteMultiByteValue(destination + 8, 2, token, 0);
model.UpdateArrayPointer(token, default, default, -1, this.start + 1, destination);
address = destination;
this.displayAddress = $"<{address:X6}>";
NotifyPropertyChanged(nameof(Address));
Load();
}
viewPort.ChangeHistory.CurrentChange.ChangeData(viewPort.Model, start, (byte)scriptType);
NotifyPropertyChanged(nameof(HasSubScripts));
});
}
public string Address { get => displayAddress; set => Set(ref displayAddress, value, arg => {
if (displayAddress.TryParseHex(out var result)) {
address = result;
viewPort.Model.UpdateArrayPointer(viewPort.ChangeHistory.CurrentChange, default, default, default, start + 1, address);
Load();
}
}); }
public IReadOnlyCollection<int> GetScripts() {
var results = new List<int>();
foreach (var script in SubScripts) {
results.Add(viewPort.Model.ReadPointer(script.Start + 4));
}
if (scriptType != 2 && scriptType != 4) {
results.Add(address);
}
return results;
}
public void AddSubScript() {
var model = viewPort.Model;
var token = viewPort.ChangeHistory.CurrentChange;
var run = model.GetNextRun(address);
if (run is not ITableRun tableRun) return;
tableRun = tableRun.Append(token, 1);
model.ObserveRunWritten(token, tableRun);
// add new element data
var newScriptStart = model.FindFreeSpace(model.FreeSpaceStart, 1);
token.ChangeData(model, newScriptStart, 0x02);
model.UpdateArrayPointer(token, default, default, -1, tableRun.Start + (tableRun.ElementCount - 1) * tableRun.ElementLength + 4, newScriptStart);
model.ObserveRunWritten(token, new XSERun(newScriptStart));
address = tableRun.Start;
displayAddress = $"<{address:X6}>";
NotifyPropertyChanged(nameof(Address));
if (run.Start != tableRun.Start) {
Load();
} else {
SubScripts.Append(new MapSubScriptViewModel(viewPort, tableRun.Start + tableRun.ElementCount * tableRun.ElementLength - 4));
}
}
public void Delete() {
var args = new MapScriptDeleteEventArgs();
DeleteMe.Raise(this, args);
if (!args.Success) return;
// NOTE maybe we should clear all the data in this script right here
}
public void Goto() => viewPort.Goto.Execute(address);
private void HandleDelete(object sender, MapScriptDeleteEventArgs e) {
var model = viewPort.Model;
var token = viewPort.ChangeHistory.CurrentChange;
var script = (MapSubScriptViewModel)sender;
var index = SubScripts.IndexOf(script);
if (index == -1) return;
var tableRun = model.GetNextRun(address) as ITableRun;
if (tableRun == null) return;
var table = new ModelTable(model, tableRun, () => token); // type. pointer<>
for (int i = index; i < SubScripts.Count - 1; i++) {
table[i].SetValue(0, table[i + 1].GetValue(0));
table[i].SetValue(1, table[i + 1].GetValue(1));
table[i].SetAddress("pointer", table[i + 1].GetAddress("pointer"));
}
tableRun = tableRun.Append(token, -1);
model.ObserveRunWritten(token, tableRun);
SubScripts[index].DeleteMe -= HandleDelete;
SubScripts.RemoveAt(index);
e.Success = true;
}
}
/// <summary>
/// Represents an indivdual map script from a map script table (type 2 or type 4).
/// </summary>
public class MapSubScriptViewModel : ViewModelCore {
private readonly IEditableViewPort viewPort;
private int start, variable, val, address;
private string variableText, valueText, addressText;
public int Start => start;
public event EventHandler<MapScriptDeleteEventArgs> DeleteMe;
public MapSubScriptViewModel(IEditableViewPort viewPort, int start) {
(this.viewPort, this.start) = (viewPort, start);
this.variable = viewPort.Model.ReadMultiByteValue(start, 2);
this.val = viewPort.Model.ReadMultiByteValue(start + 2, 2);
this.address = viewPort.Model.ReadPointer(start + 4);
variableText = variable.ToString("X4");
valueText = val.ToString();
addressText = $"<{address:X6}>";
}
public string Variable { get => variableText; set => Set(ref variableText, value, arg => {
if (!variableText.TryParseHex(out int result)) return;
variable = result;
viewPort.Model.WriteMultiByteValue(start, 2, viewPort.ChangeHistory.CurrentChange, variable);
}); }
public string Value { get => valueText; set => Set(ref valueText, value, arg => {
if (!int.TryParse(valueText, out int result)) return;
val = result;
viewPort.Model.WriteMultiByteValue(start + 2, 2, viewPort.ChangeHistory.CurrentChange, val);
}); }
public string Address { get => addressText; set => Set(ref addressText, value, arg => {
var text = addressText.Trim("<> ".ToCharArray());
if (!text.TryParseHex(out int result)) return;
// do the same work that we do in the code tool, removing scripts that aren't needed
address = result;
viewPort.Model.UpdateArrayPointer(viewPort.ChangeHistory.CurrentChange, default, default, -1, start + 4, address);
}); }
public void Delete() {
var args = new MapScriptDeleteEventArgs();
DeleteMe.Raise(this, args);
if (!args.Success) return;
// leaving an orphan behind on purpose
}
public void Goto() => viewPort.Goto.Execute(address);
}
}