new features

* squiggle errors for pokemon names / moves in trainer editor
* filtering for block behaviors
* image editor hover position and selection size displayed
* name hints for blocksets in the map header
This commit is contained in:
haven1433 2025-12-12 21:27:19 -06:00
parent 19df9c0e24
commit 47d5aa830d
29 changed files with 300 additions and 53 deletions

View File

@ -155,7 +155,7 @@ namespace HavenSoft.HexManiac.Core {
foreach (var item in items) set.Add(item);
}
public static bool All<T>(this ReadOnlySpan<T> span, Func<T,bool> predicate) {
public static bool All<T>(this ReadOnlySpan<T> span, Func<T, bool> predicate) {
var match = true;
for (int i = 0; match && i < span.Length; i++) {
match = predicate(span[i]);
@ -212,6 +212,8 @@ namespace HavenSoft.HexManiac.Core {
return result;
}
public static void CombineTokens(this IList<string> tokens, string startToken, string endToken) => TableStreamRun.Recombine(tokens, startToken, endToken);
public static string CombineLines(this IReadOnlyList<string> lines) => lines.Aggregate((a, b) => a + Environment.NewLine + b);
public static string ReplaceOne(this string input, string search, string replacement) {

View File

@ -839,6 +839,8 @@ namespace HavenSoft.HexManiac.Core.Models {
public static IReadOnlyList<string> GetBitOptions(this IDataModel model, string tableName) => ModelCacheScope.GetCache(model).GetBitOptions(tableName);
public static bool TryMatch(this IReadOnlyList<string> list, string text, out int value) => ArrayRunEnumSegment.TryMatch(text, list, out value);
public static IEnumerable<ArrayRun> GetRelatedArrays(this IDataModel model, ArrayRun table) {
yield return table; // a table is related to itself
var basename = model.GetAnchorFromAddress(-1, table.Start);

View File

@ -221,7 +221,11 @@ namespace HavenSoft.HexManiac.Core.Models {
public ModelTupleElement GetTuple(string fieldName) {
var seg = table.ElementContent.Single(segment => segment.Name == fieldName);
var segmentOffset = table.ElementContent.Until(s => s == seg).Sum(s => s.Length);
return new ModelTupleElement(model, table, arrayIndex, segmentOffset, (ArrayRunTupleSegment)seg, tokenFactory);
if (seg is ArrayRunBitArraySegment bitSeg) {
return new ModelTupleElement(model, table, arrayIndex, segmentOffset, (ArrayRunBitArraySegment)seg, tokenFactory);
} else {
return new ModelTupleElement(model, table, arrayIndex, segmentOffset, (ArrayRunTupleSegment)seg, tokenFactory);
}
}
public object __getindex__(string key) => this[key]; // for python
@ -459,7 +463,7 @@ namespace HavenSoft.HexManiac.Core.Models {
SetValue(fieldName, (int)value);
}
}
public int FieldCount => tuple.Elements.Count;
public bool HasField(string name) => tuple.Elements.Any(field => field.Name == name);
public int GetValue(string fieldName) {
@ -618,6 +622,7 @@ namespace HavenSoft.HexManiac.Core.Models {
public bool is_pokemon => eggRun.CreateDataFormat(model, address) is EggSection;
public bool is_move => eggRun.CreateDataFormat(model, address) is EggItem;
public int Address => address;
public EggElement(IDataModel model, int address, Func<ModelDelta> tokenFactory, EggMoveRun eggRun) => (this.model, this.address, this.tokenFactory, this.eggRun) = (model, address, tokenFactory, eggRun);

View File

@ -1,4 +1,5 @@
using HavenSoft.HexManiac.Core.ViewModels.DataFormats;
using HavenSoft.HexManiac.Core.ViewModels;
using HavenSoft.HexManiac.Core.ViewModels.DataFormats;
using HavenSoft.HexManiac.Core.ViewModels.Images;
using System.Collections.Generic;
using System.Text;
@ -16,6 +17,7 @@ namespace HavenSoft.HexManiac.Core.Models.Runs {
public override string FormatString => "`asc`" + Length;
public IReadOnlyList<IPixelViewModel> Visualizations => throw new System.NotImplementedException();
public ITextPreProcessor PreFormatter { get; }
public AsciiRun(IDataModel model, int start, int length, SortedSpan<int> pointerSources = null) : base(start, pointerSources) => (this.model, Length) = (model, length.LimitToRange(1, 1000));

View File

@ -1,4 +1,5 @@
using HavenSoft.HexManiac.Core.ViewModels.DataFormats;
using HavenSoft.HexManiac.Core.ViewModels;
using HavenSoft.HexManiac.Core.ViewModels.DataFormats;
using HavenSoft.HexManiac.Core.ViewModels.Images;
using System.Collections.Generic;
using System.Linq;
@ -55,6 +56,7 @@ namespace HavenSoft.HexManiac.Core.Models.Runs {
public override string FormatString => SharedFormatString;
public IReadOnlyList<IPixelViewModel> Visualizations => null;
public ITextPreProcessor PreFormatter { get; }
public void AppendTo(IDataModel model, StringBuilder builder, int start, int length, int depth) {
for (int i = 0; i < length - 1; i++) {

View File

@ -1,4 +1,5 @@
using HavenSoft.HexManiac.Core.ViewModels.DataFormats;
using HavenSoft.HexManiac.Core.ViewModels;
using HavenSoft.HexManiac.Core.ViewModels.DataFormats;
using HavenSoft.HexManiac.Core.ViewModels.Images;
using System;
using System.Collections.Generic;
@ -213,6 +214,7 @@ namespace HavenSoft.HexManiac.Core.Models.Runs {
}
public IReadOnlyList<IPixelViewModel> Visualizations => new List<IPixelViewModel>();
public ITextPreProcessor PreFormatter { get; }
public bool DependsOn(string anchorName) {
return anchorName == PokemonNameTable || anchorName == MoveNamesTable;
}

View File

@ -1,4 +1,5 @@
using HavenSoft.HexManiac.Core.ViewModels.DataFormats;
using HavenSoft.HexManiac.Core.ViewModels;
using HavenSoft.HexManiac.Core.ViewModels.DataFormats;
using HavenSoft.HexManiac.Core.ViewModels.Images;
using System;
using System.Collections;
@ -66,6 +67,8 @@ namespace HavenSoft.HexManiac.Core.Models.Runs {
IReadOnlyList<AutocompleteItem> GetAutoCompleteOptions(string line, int caretLineIndex, int caretCharacterIndex);
IReadOnlyList<IPixelViewModel> Visualizations { get; }
ITextPreProcessor PreFormatter { get; }
}
public class FormattedRunComparer : IComparer<IFormattedRun> {

View File

@ -138,6 +138,15 @@ namespace HavenSoft.HexManiac.Core.Models.Runs {
return results;
}
private IReadOnlyDictionary<int, int> devolutions, minimumLevel;
private IReadOnlyDictionary<int, IReadOnlyList<int>> eggMoves, levelupMoves, tutorMoves, tmMoves;
public IReadOnlyDictionary<int, int> GetPokemonDevolutions() => devolutions ??= Flags.GetPokemonDevolutions(model);
public IReadOnlyDictionary<int, int> GetPokemonMinimumLevels() => minimumLevel ??= Flags.GetMinimumLevelForPokemon(model);
public IReadOnlyDictionary<int, IReadOnlyList<int>> GetPokemonEggMoves() => eggMoves ??= Flags.GetPokemonEggMoves(model);
public IReadOnlyDictionary<int, IReadOnlyList<int>> GetLevelupMoves() => levelupMoves ??= Flags.GetPokemonLevelupMoves(model);
public IReadOnlyDictionary<int, IReadOnlyList<int>> GetTutorMoves() => tutorMoves ??= Flags.GetPokemonTutorMoves(model);
public IReadOnlyDictionary<int, IReadOnlyList<int>> GetTmMoves() => tmMoves ??= Flags.GetPokemonTmMoves(model);
#region Script Cache
// stores only the start and length

View File

@ -1,4 +1,5 @@
using HavenSoft.HexManiac.Core.ViewModels.DataFormats;
using HavenSoft.HexManiac.Core.ViewModels;
using HavenSoft.HexManiac.Core.ViewModels.DataFormats;
using HavenSoft.HexManiac.Core.ViewModels.Images;
using System;
using System.Collections.Generic;
@ -79,6 +80,7 @@ namespace HavenSoft.HexManiac.Core.Models.Runs {
}
public IReadOnlyList<IPixelViewModel> Visualizations => new List<IPixelViewModel>();
public ITextPreProcessor PreFormatter { get; }
public bool DependsOn(string anchorName) => false;
protected override BaseRun Clone(SortedSpan<int> newPointerSources) => new PCSRun(model, Start, Length, newPointerSources);

View File

@ -1,4 +1,5 @@
using HavenSoft.HexManiac.Core.Models.Runs.Factory;
using HavenSoft.HexManiac.Core.ViewModels;
using HavenSoft.HexManiac.Core.ViewModels.DataFormats;
using HavenSoft.HexManiac.Core.ViewModels.Images;
using System;
@ -146,7 +147,7 @@ namespace HavenSoft.HexManiac.Core.Models.Runs {
get => GetBit(Start + 5, 7);
set => SetBit(Start + 5, 7, value);
}
public bool ChangeHappinessWhenBetween100And199{
public bool ChangeHappinessWhenBetween100And199 {
get => GetBit(Start + 5, 6);
set => SetBit(Start + 5, 6, value);
}
@ -462,6 +463,7 @@ namespace HavenSoft.HexManiac.Core.Models.Runs {
}
public IReadOnlyList<IPixelViewModel> Visualizations => new List<IPixelViewModel>();
public ITextPreProcessor PreFormatter { get; }
public bool DependsOn(string anchorName) => false;
public void AppendTo(IDataModel model, StringBuilder builder, int start, int length, int depth) {

View File

@ -1,4 +1,5 @@
using HavenSoft.HexManiac.Core.ViewModels.DataFormats;
using HavenSoft.HexManiac.Core.ViewModels;
using HavenSoft.HexManiac.Core.ViewModels.DataFormats;
using HavenSoft.HexManiac.Core.ViewModels.Images;
using System;
using System.Collections.Generic;
@ -130,6 +131,7 @@ namespace HavenSoft.HexManiac.Core.Models.Runs {
}
public IReadOnlyList<IPixelViewModel> Visualizations => new List<IPixelViewModel>();
public ITextPreProcessor PreFormatter { get; }
public bool DependsOn(string anchorName) => anchorName == HardcodeTablesModel.MoveNamesTable;
public IEnumerable<(int, int)> Search(int index) {

View File

@ -1,4 +1,5 @@
using HavenSoft.HexManiac.Core.ViewModels.DataFormats;
using HavenSoft.HexManiac.Core.ViewModels;
using HavenSoft.HexManiac.Core.ViewModels.DataFormats;
using HavenSoft.HexManiac.Core.ViewModels.Images;
using System;
using System.Collections.Generic;
@ -218,6 +219,7 @@ namespace HavenSoft.HexManiac.Core.Models.Runs.Sprites {
}
public IReadOnlyList<IPixelViewModel> Visualizations => new List<IPixelViewModel>();
public ITextPreProcessor PreFormatter { get; }
public bool DependsOn(string anchorName) => false;
#endregion

View File

@ -196,7 +196,7 @@ namespace HavenSoft.HexManiac.Core.Models.Runs {
return result;
}
public TableStreamRun DeserializeRunFromZero(string content,ModelDelta token, out IReadOnlyList<int> changedOffsets, out List<int> movedChildren) {
public TableStreamRun DeserializeRunFromZero(string content, ModelDelta token, out IReadOnlyList<int> changedOffsets, out List<int> movedChildren) {
return DeserializeRun(content, token, 0, out changedOffsets, out movedChildren);
}
@ -361,7 +361,7 @@ namespace HavenSoft.HexManiac.Core.Models.Runs {
return tokens;
}
public static void Recombine(List<string> tokens, string startToken, string endToken) {
public static void Recombine(IList<string> tokens, string startToken, string endToken) {
for (int i = 0; i < tokens.Count - 1; i++) {
if (tokens[i].StartsWith(startToken) == tokens[i].EndsWith(endToken)) continue;
tokens[i] += " " + tokens[i + 1];
@ -493,6 +493,7 @@ namespace HavenSoft.HexManiac.Core.Models.Runs {
}
public IReadOnlyList<IPixelViewModel> Visualizations => new List<IPixelViewModel>();
public ITextPreProcessor PreFormatter { get; }
#endregion

View File

@ -1,8 +1,8 @@
using HavenSoft.HexManiac.Core.Models.Code;
using HavenSoft.HexManiac.Core.Models.Runs.Sprites;
using HavenSoft.HexManiac.Core.ViewModels;
using HavenSoft.HexManiac.Core.ViewModels.DataFormats;
using HavenSoft.HexManiac.Core.ViewModels.Images;
using Mono.Unix.Native;
using System;
using System.Collections.Generic;
using System.Linq;
@ -36,6 +36,7 @@ namespace HavenSoft.HexManiac.Core.Models.Runs {
string IUpdateFromParentRun.RepointContentShortName => "Team";
private readonly bool showFullIVByteRange = false;
private object minimumLevels;
#region Constructors
@ -377,6 +378,8 @@ namespace HavenSoft.HexManiac.Core.Models.Runs {
}
}
public ITextPreProcessor PreFormatter => new TrainerTextFormatter(model);
public bool DependsOn(string anchorName) =>
anchorName == HardcodeTablesModel.ItemsTableName ||
anchorName == HardcodeTablesModel.MoveNamesTable ||
@ -596,4 +599,70 @@ namespace HavenSoft.HexManiac.Core.Models.Runs {
return results;
}
}
public record TrainerTextFormatter(IDataModel Model) : ITextPreProcessor {
public TextFormatting[] Format(string content) => new TextFormatting[0];
public IEnumerable<TextSegment> FindErrors(string content) {
var errors = new List<TextSegment>();
int species = 0;
var lines = content.SplitLines();
for (int i = 0; i < lines.Length; i++) {
if (string.IsNullOrWhiteSpace(lines[i])) continue;
if (lines[i].Trim().StartsWith("-")) {
CheckMoveError(species, i, lines[i], errors);
} else {
species = CheckPokemonError(i, lines[i], errors);
}
}
return errors;
}
private int CheckPokemonError(int lineNumber, string line, IList<TextSegment> errors) {
var tokens = line.Split(' ', StringSplitOptions.RemoveEmptyEntries).ToList();
tokens.CombineTokens("\"", "\"");
if (tokens.Count < 2) return 0;
if (!int.TryParse(tokens[0], out var level)) return 0;
var cache = ModelCacheScope.GetCache(Model);
var pokemonNames = cache.GetOptions(HardcodeTablesModel.PokemonNameTable);
if (!pokemonNames.TryMatch(tokens[1], out var pokemon)) return 0;
// error: pokemon is too low of a level
var minimumLevels = cache.GetPokemonMinimumLevels();
if (minimumLevels is not null) {
if (minimumLevels.TryGetValue(pokemon, out var minLevel) && minLevel > level) {
errors.Add(new(lineNumber, line.IndexOf(tokens[1]), tokens[1].Length, SegmentType.Warning));
} else if (cache.GetPokemonDevolutions()?.TryGetValue(pokemon, out var baby) ?? false) {
// check that the lower evolution is also allowed. Important for stage 3 pokmeon that evolve via stone
if (minimumLevels.TryGetValue(baby, out var babyLevel) && babyLevel > level) {
errors.Add(new(lineNumber, line.IndexOf(tokens[1]), tokens[1].Length, SegmentType.Warning));
}
}
}
return pokemon;
}
private void CheckMoveError(int species, int lineNumber, string line, IList<TextSegment> errors) {
var moveName = line.Substring(1).Trim();
var cache = ModelCacheScope.GetCache(Model);
var moveNames = cache.GetOptions(HardcodeTablesModel.MoveNamesTable);
if (!moveNames.TryMatch(line, out var move)) return;
var allMoves = new HashSet<int>();
for (int i = 0; i < 5; i++) {
if (cache.GetLevelupMoves()?.TryGetValue(species, out var lvlMoves) ?? false) allMoves.AddRange(lvlMoves);
if (cache.GetTutorMoves()?.TryGetValue(species, out var tutMoves) ?? false) allMoves.AddRange(tutMoves);
if (cache.GetPokemonEggMoves()?.TryGetValue(species, out var eggMoves) ?? false) allMoves.AddRange(eggMoves);
if (cache.GetTmMoves()?.TryGetValue(species, out var tmMoves) ?? false) allMoves.AddRange(tmMoves);
if (!(cache.GetPokemonDevolutions()?.TryGetValue(species, out var baby) ?? false)) break;
species = baby;
}
// error: pokemon cannot learn move
if (!allMoves.Contains(move) && line.Length > 2) {
errors.Add(new(lineNumber, 2, line.Length - 2, SegmentType.Warning));
}
}
}
}

View File

@ -399,6 +399,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels {
/// <param name="toSelect">Points range from (0,0) to (PixelWidth, PixelHeight) </param>
private void RaiseRefreshSelection(params Point[] toSelect) {
SelectionSize = Point.Zero;
selectedPixels = new bool[PixelWidth, PixelHeight];
foreach (var s in toSelect) {
if (WithinImage(s)) selectedPixels[s.X, s.Y] = true;
@ -418,6 +419,10 @@ namespace HavenSoft.HexManiac.Core.ViewModels {
private double spriteScale = 4;
public double SpriteScale { get => spriteScale; set => Set(ref spriteScale, value, arg => NotifyPropertyChanged(nameof(FontSize))); }
private string quickInfo = string.Empty;
public string QuickInfo { get => quickInfo; set => Set(ref quickInfo, value); }
public Point SelectionSize { get; private set; }
public PaletteCollection Palette { get; }
public int SpritePointer { get; private set; }
@ -595,6 +600,16 @@ namespace HavenSoft.HexManiac.Core.ViewModels {
} else {
toolStrategy.ToolDrag(point);
}
QuickInfo = string.Empty;
point = ToSpriteSpace(point);
if (!point.X.InRange(0, PixelWidth)) return;
if (!point.Y.InRange(0, PixelHeight)) return;
var text = point.ToString();
if (SelectionSize.X > 1 || SelectionSize.Y > 1) {
text = $"[{SelectionSize}]";
}
QuickInfo = text;
}
public void ToolUp(Point point) {
@ -1048,6 +1063,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels {
var selectionPoints = new Point[width * height];
for (int x = 0; x < width; x++) for (int y = 0; y < height; y++) selectionPoints[y * width + x] = start + new Point(x, y);
parent.RaiseRefreshSelection(selectionPoints);
parent.SelectionSize = new Point(width, height);
}
public void ClearSelection() {

View File

@ -6,6 +6,7 @@ using HexManiac.Core.Models.Runs.Sprites;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
/*
#define MB_NORMAL 0x00
@ -431,7 +432,8 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Map {
hasTerrainAndEncounter = blockAttributes[0].Length > 2;
images = new CanvasPixelViewModel[8];
indexForTileImage = new Dictionary<IPixelViewModel, int>();
if (listSource.TryGetList("MapAttributeBehaviors", out var behaviors)) behaviors.ForEach(BehaviorOptions.Add);
if (listSource.TryGetList("MapAttributeBehaviors", out var behaviors)) BehaviorOptions.Update(behaviors.Select((behavior, i) => new ComboOption(behavior, i)), behavior);
BehaviorOptions.Bind(nameof(BehaviorOptions.SelectedIndex), (obj, e) => Behavior = BehaviorOptions.SelectedIndex);
if (listSource.TryGetList("MapLayerOptions", out var layer)) layer.ForEach(LayerOptions.Add);
if (listSource.TryGetList("MapTerrainOptions", out var terrain)) terrain.ForEach(TerrainOptions.Add);
if (listSource.TryGetList("MapEncounterOptions", out var encounters)) encounters.ForEach(EncounterOptions.Add);
@ -594,7 +596,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Map {
public bool HasError => errorText != null;
public string ErrorText => errorText;
public ObservableCollection<string> BehaviorOptions { get; } = new();
public FilteringComboOptions BehaviorOptions { get; } = new();
public ObservableCollection<string> LayerOptions { get; } = new();
public ObservableCollection<string> TerrainOptions { get; } = new();
public ObservableCollection<string> EncounterOptions { get; } = new();
@ -612,6 +614,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Map {
}
errorText = attributes.ErrorInfo;
NotifyPropertiesChanged(nameof(Behavior), nameof(Layer), nameof(Terrain), nameof(Encounter), nameof(HasError), nameof(ErrorText));
BehaviorOptions.Update(BehaviorOptions.AllOptions, behavior);
}
private void SaveAttributes(int arg = default) {

View File

@ -267,6 +267,8 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Map {
return trainerFlags;
}
#region Derived Pokemon Data
public static IReadOnlyDictionary<int, int> GetMinimumLevelForPokemon(IDataModel model) {
var evolutions = model.GetTableModel(HardcodeTablesModel.EvolutionTableName);
var levelMethods = new[] { 4, 8, 9, 10, 11, 12, 13, 14 };
@ -284,6 +286,101 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Map {
return results;
}
/// <summary>
/// For a given pokemon, find what pokemon (if any evolves into it)
/// </summary>
public static IReadOnlyDictionary<int, int> GetPokemonDevolutions(IDataModel model) {
var evolutions = model.GetTableModel(HardcodeTablesModel.EvolutionTableName);
var levelMethods = new[] { 4, 8, 9, 10, 11, 12, 13, 14 };
var results = new Dictionary<int, int>();
foreach (var evo in evolutions) {
// method1:evolutionmethods arg1:|s=method1(6=data.items.stats|7=data.items.stats) species1:data.pokemon.names unused1:
for (int i = 0; i < evo.Length; i += 8) {
var species = model.ReadMultiByteValue(evo.Start + i + 4, 2);
results[species] = evo.ArrayIndex;
}
}
return results;
}
public static IReadOnlyDictionary<int, IReadOnlyList<int>> GetPokemonEggMoves(IDataModel model) {
var start = model.GetAddressFromAnchor(new NoDataChangeDeltaModel(), -1, HardcodeTablesModel.EggMovesTableName);
if (!start.InRange(0, model.Count)) return null;
if (model.GetNextRun(start) is not EggMoveRun eggRun) return null;
var table = new EggTable(model, null, eggRun);
int currentPokemon = -1;
var results = new AutoDictionary<int, List<int>>(i => new());
foreach (var element in table) {
var value = model.ReadMultiByteValue(element.Address, 2);
if (element.is_pokemon) {
value -= EggMoveRun.MagicNumber;
currentPokemon = value;
} else {
results[currentPokemon].Add(value);
}
}
return new Dictionary<int, IReadOnlyList<int>>(results.Select(kvp => new KeyValuePair<int, IReadOnlyList<int>>(kvp.Key, kvp.Value)));
}
public static IReadOnlyDictionary<int, IReadOnlyList<int>> GetPokemonLevelupMoves(IDataModel model) {
var start = model.GetAddressFromAnchor(new NoDataChangeDeltaModel(), -1, HardcodeTablesModel.LevelMovesTableName);
if (!start.InRange(0, model.Count)) return null;
if (model.GetNextRun(start) is not ArrayRun array) return null;
var table = new ModelTable(model, array);
var results = new AutoDictionary<int, List<int>>(i => new());
foreach (var element in table) {
var sub = element.GetSubTable("movesFromLevel");
foreach (var move in sub) {
if (sub.Run.ElementContent.Count == 1) {
results[element.ArrayIndex].Add(model.ReadMultiByteValue(move.Start, 2) & 0x1FF);
} else if (move.TryGetValue("move", out int moveIndex)) {
results[element.ArrayIndex].Add(moveIndex);
}
}
}
return new Dictionary<int, IReadOnlyList<int>>(results.Select(kvp => new KeyValuePair<int, IReadOnlyList<int>>(kvp.Key, kvp.Value)));
}
public static IReadOnlyDictionary<int, IReadOnlyList<int>> GetPokemonTutorMoves(IDataModel model) {
var start = model.GetAddressFromAnchor(new NoDataChangeDeltaModel(), -1, HardcodeTablesModel.TutorCompatibility);
if (!start.InRange(0, model.Count)) return null;
if (model.GetNextRun(start) is not ArrayRun array) return null;
var table = new ModelTable(model, array);
var options = model.GetTableModel(HardcodeTablesModel.MoveTutors);
if (options == null) return null;
var results = new AutoDictionary<int, List<int>>(i => new());
foreach (var element in table) {
if (!element.HasField("moves")) break;
var move = element.GetTuple("moves");
for (int i = 0; i < move.FieldCount; i++) {
if ((model[element.Start + i / 8] & (1 << i)) == 0) continue;
results[element.ArrayIndex].Add(options[i].GetValue(0));
}
}
return new Dictionary<int, IReadOnlyList<int>>(results.Select(kvp => new KeyValuePair<int, IReadOnlyList<int>>(kvp.Key, kvp.Value)));
}
public static IReadOnlyDictionary<int, IReadOnlyList<int>> GetPokemonTmMoves(IDataModel model) {
var start = model.GetAddressFromAnchor(new NoDataChangeDeltaModel(), -1, HardcodeTablesModel.TmCompatibility);
if (!start.InRange(0, model.Count)) return null;
if (model.GetNextRun(start) is not ArrayRun array) return null;
var table = new ModelTable(model, array);
var options = model.GetTableModel(HardcodeTablesModel.TmMoves);
if (options == null) return null;
var results = new AutoDictionary<int, List<int>>(i => new());
foreach (var element in table) {
if (!element.HasField("moves")) break;
var move = element.GetTuple("moves");
for (int i = 0; i < move.FieldCount; i++) {
if ((model[element.Start + i / 8] & (1 << i)) == 0) continue;
results[element.ArrayIndex].Add(options[i].GetValue(0));
}
}
return new Dictionary<int, IReadOnlyList<int>>(results.Select(kvp => new KeyValuePair<int, IReadOnlyList<int>>(kvp.Key, kvp.Value)));
}
#endregion
public static ISet<int> GetTrainerFlagUsages(IDataModel model, ScriptParser parser, int flag) {
var flagUsages = new HashSet<int>();
foreach (var spot in GetAllScriptSpots(model, parser, GetAllTopLevelScripts(model), 0x5C)) {

View File

@ -885,6 +885,8 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Map {
public bool ShowTrainerContent => EventTemplate.GetTrainerContent(element.Model, this) != null && TrainerType != 0;
public TextEditorViewModel TrainerContent { get; } = new();
public int TrainerClass {
get {
var trainerContent = EventTemplate.GetTrainerContent(element.Model, this);
@ -1400,6 +1402,10 @@ show:
legendaryContent = new Lazy<LegendaryEventContent>(() => EventTemplate.GetLegendaryEventContent(element.Model, parser, this));
UpdateScriptError(ScriptAddress);
TrainerContent.PreFormatter = new TrainerTextFormatter(objectEvent.Model);
TrainerContent.Content = TrainerTeam;
TrainerContent.Bind(nameof(TextEditorViewModel.Content), (sender, e) => TrainerTeam = sender.Content);
}
public override int TopOffset => 16 - (EventRender?.PixelHeight ?? 0);
@ -1948,6 +1954,9 @@ show:
public TrainerTeamViewModel(IDataModel model, int trainerID, Func<ModelDelta> tokenGenerator) {
(this.model, this.trainerID) = (model, trainerID);
this.tokenGenerator = tokenGenerator;
TrainerContent.PreFormatter = new TrainerTextFormatter(model);
TrainerContent.Content = TrainerTeam;
TrainerContent.Bind(nameof(TextEditorViewModel.Content), (sender, e) => TrainerTeam = sender.Content);
}
public string TrainerIDText {
@ -1987,6 +1996,8 @@ show:
}
}
public TextEditorViewModel TrainerContent { get; } = new();
public ObservableCollection<IPixelViewModel> TeamVisualizations { get; } = new();
private void UpdateTeamVisualizations(TrainerPokemonTeamRun team) {

View File

@ -1,5 +1,6 @@
using HavenSoft.HexManiac.Core.Models;
using HavenSoft.HexManiac.Core.Models.Map;
using HavenSoft.HexManiac.Core.Models.Runs;
using HavenSoft.HexManiac.Core.ViewModels.Images;
using HexManiac.Core.Models.Runs.Sprites;
using System;
@ -173,14 +174,14 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Map {
public bool HasMapTypeOptions => MapTypeOptions.Count > 0;
public ObservableCollection<string> MapTypeOptions { get; } = new();
private int GetValue([CallerMemberName]string name = null) {
private int GetValue([CallerMemberName] string name = null) {
name = char.ToLower(name[0]) + name.Substring(1);
if (!map.HasField(name)) return -1;
return map.GetValue(name);
}
// when we call SetValue, get the latest token
private void SetValue(int value, [CallerMemberName]string name = null) {
private void SetValue(int value, [CallerMemberName] string name = null) {
if (value == GetValue(name)) return;
map = new(map.Model, map.Table.Start, (map.Start - map.Table.Start) / map.Table.ElementCount, tokenFactory, map.Table);
var originalName = name;
@ -189,7 +190,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Map {
NotifyPropertyChanged(originalName);
}
private bool GetBool([CallerMemberName]string name = null) {
private bool GetBool([CallerMemberName] string name = null) {
name = char.ToLower(name[0]) + name.Substring(1);
if (map.HasField(name)) {
return map.GetValue(name) != 0;
@ -202,7 +203,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Map {
return false;
}
private void SetBool(bool value, [CallerMemberName]string name = null) {
private void SetBool(bool value, [CallerMemberName] string name = null) {
var originalName = name;
name = char.ToLower(name[0]) + name.Substring(1);
if (map.HasField(name)) {
@ -222,12 +223,22 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Map {
private readonly Lazy<IPixelViewModel> render;
public IDataModel Model { get; }
public int Address { get; }
public string NameHint { get; }
public IPixelViewModel Render => render?.Value;
public string AddressText => Address.ToAddress();
public string AddressText => Address.ToAddress() + NameHint;
public BlocksetOption(IDataModel model, int address) {
Model = model;
Address = address;
var sources = model.GetNextRun(address)?.PointerSources ?? Enumerable.Empty<int>();
var layoutRuns = sources.Select(source => model.GetNextRun(source)).Distinct();
var layoutSources = layoutRuns.SelectMany(run => run?.PointerSources ?? Enumerable.Empty<int>());
var mapRuns = layoutSources.Select(source => model.GetNextRun(source)).Where(run => model.GetAnchorFromAddress(-1, run.Start) != "data.maps.layouts" && run is ITableRun);
var elements = mapRuns.Select(run => new ModelTable(model, (ITableRun)run)[0]).Where(element => element.HasField("regionSectionID"));
NameHint = elements.Select(element => element.GetEnumValue("regionSectionID")).Distinct().OrderBy(s => s).FirstOrDefault() ?? string.Empty;
if (NameHint != string.Empty) NameHint = $" (ex. {NameHint})";
if (!model.SpartanMode) render = new Lazy<IPixelViewModel>(() => new BlocksetModel(Model, Address).RenderBlockset(.5));
}
}

View File

@ -8,6 +8,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels {
public enum TextFormatting { None, Keyword, Constant, Numeric, Comment, Text }
public interface ITextPreProcessor {
TextFormatting[] Format(string content);
IEnumerable<TextSegment> FindErrors(string content);
}
public class TextEditorViewModel : ViewModelCore {
@ -44,6 +45,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels {
public string Content {
get => content;
set {
value ??= string.Empty;
if (content == value) return;
var oldContent = content;
content = value;
@ -78,6 +80,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels {
public void FocusKeyboard() => RequestKeyboardFocus.Raise(this);
private void UpdateLayers() {
ErrorLocations.Clear();
if (content.Length == 0) {
if (PlainContent.Length != 0) {
PlainContent = AccentContent =
@ -113,6 +116,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels {
if (pre[i] == TextFormatting.Text) text.Replace(i, basic, i, 1);
basic.Clear(i, 1);
}
foreach (var error in PreFormatter.FindErrors(Content)) ErrorLocations.Add(error);
}
// comments
@ -179,6 +183,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels {
NumericContent = numeric.ToString();
CommentContent = comments.ToString();
TextContent = text.ToString();
NotifyPropertiesChanged(
nameof(PlainContent),
nameof(AccentContent),

View File

@ -585,6 +585,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Tools {
}
return result;
}
public IEnumerable<TextSegment> FindErrors(string content) { yield break; }
}
public record HelpContext(string Line, int Index, int ContentBoundaryCount = 0, int ContentBoundaryIndex = -1, bool IsSelection = false);

View File

@ -3,6 +3,7 @@ using HavenSoft.HexManiac.Core.Models.Runs;
using Microsoft.Scripting.Hosting;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Dynamic;
@ -150,6 +151,7 @@ clr.ImportExtensions(HavenSoft.HexManiac.Core.Models)
}
return result;
}
public IEnumerable<TextSegment> FindErrors(string content) { yield break; }
}
public record TableGetter(EditorViewModel Editor) {

View File

@ -26,6 +26,8 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Tools {
}
}
public TextEditorViewModel ElementContent { get; } = new();
protected void HandleNewDataStream(IStreamRun oldRun, IStreamRun newRun, IReadOnlyList<int> changedOffsets, IReadOnlyList<int> changedRuns) {
Model.ObserveRunWritten(ViewPort.CurrentChange, newRun);
if (oldRun.Start != newRun.Start) {
@ -33,7 +35,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Tools {
}
if (Model.GetNextRun(newRun.Start) is ITableRun table) {
foreach(var change in changedOffsets) {
foreach (var change in changedOffsets) {
var offsets = table.ConvertByteOffsetToArrayOffset(change);
var info = table.NotifyChildren(Model, ViewPort.CurrentChange, offsets.ElementIndex, offsets.SegmentIndex);
ViewPort.HandleErrorInfo(info);
@ -72,6 +74,9 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Tools {
} else {
content = string.Empty;
}
ElementContent.Content = content;
ElementContent.Bind(nameof(ElementContent.Content), (sender, e) => Content = ElementContent.Content);
}
protected override bool TryCopy(StreamElementViewModel other) {
@ -79,6 +84,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Tools {
if (!(other is TextStreamElementViewModel stream)) return false;
Start = other.Start;
TryUpdate(ref content, stream.content, nameof(Content));
ElementContent.Content = Content;
NotifyPropertyChanged(nameof(Visualizations));
return true;
}

View File

@ -527,6 +527,7 @@
<local:SelectionRender x:Name="SelectionRender" Stretch="None" />
</local:GridDecorator>
</Canvas>
<TextBlock Background="{DynamicResource Background}" HorizontalAlignment="Right" VerticalAlignment="Bottom" Text="{Binding QuickInfo}" />
</Grid>
</DockPanel>
</UserControl>

View File

@ -520,9 +520,8 @@
</ItemsControl.ItemTemplate>
</ItemsControl>
</Viewbox>
<TextBox VerticalAlignment="Top" Text="{Binding TrainerTeam, UpdateSourceTrigger=PropertyChanged}"
HorizontalAlignment="Right" AcceptsReturn="True" FontFamily="Consolas"
Name="TrainerTeamBox" Background="Transparent" Width="230" CaretBrush="{DynamicResource Secondary}" />
<local:TextEditor x:Name="TrainerTeamBox" DataContext="{Binding TrainerContent}" Width="230"
Background="Transparent" VerticalAlignment="Top" Visibility="{Binding ShowContent, Converter={StaticResource BoolToVisibility}}" />
<local:AutocompleteOverlay Target="{Binding ElementName=TrainerTeamBox}" />
</Grid>
<local:AngleBorder Direction="Right" HorizontalAlignment="Left" Margin="4,10,0,0">
@ -569,9 +568,8 @@
</ItemsControl.ItemTemplate>
</ItemsControl>
</Viewbox>
<TextBox VerticalAlignment="Top" Text="{Binding TrainerTeam, UpdateSourceTrigger=PropertyChanged}"
HorizontalAlignment="Stretch" AcceptsReturn="True" FontFamily="Consolas"
Name="TrainerTeamBox" Background="Transparent" CaretBrush="{DynamicResource Secondary}" />
<local:TextEditor x:Name="TrainerTeamBox" DataContext="{Binding TrainerContent}"
Background="Transparent" HorizontalAlignment="Stretch" Visibility="{Binding ShowContent, Converter={StaticResource BoolToVisibility}}" />
<Button Command="{res:MethodCommand Refresh}" ToolTip="Refresh" BorderBrush="{DynamicResource Secondary}"
HorizontalAlignment="Right" VerticalAlignment="Top" Width="20" Height="20">
<Path Data="{res:Icon RotationArrow}" Fill="{DynamicResource Primary}" Stretch="Uniform" />
@ -1030,8 +1028,7 @@
<local:AngleComboBox Grid.Column="1" Direction="Out" Margin="0,2,2,2" DisplayMemberPath="AddressText"
ItemContainerStyle="{StaticResource BlocksetOptionsContainer}"
ItemsSource="{Binding PrimaryOptions}" SelectedIndex="{Binding PrimaryIndex}">
</local:AngleComboBox>
ItemsSource="{Binding PrimaryOptions}" SelectedIndex="{Binding PrimaryIndex}" />
<local:AngleComboBox Grid.Column="1" Direction="Out" Margin="0,2,2,2" Grid.Row="1" DisplayMemberPath="AddressText"
ItemContainerStyle="{StaticResource BlocksetOptionsContainer}"
ItemsSource="{Binding SecondaryOptions}" SelectedIndex="{Binding SecondaryIndex}" />

View File

@ -21,7 +21,7 @@
<RowDefinition />
</Grid.RowDefinitions>
<local:AngleBorder Direction="Left" Content="Behavior:" Margin="0,0,-2,0" />
<local:AngleComboBox Direction="Out" Grid.Column="1" SelectedIndex="{Binding Behavior}" ItemsSource="{Binding BehaviorOptions}" Margin="2" />
<local:AngleComboBox Direction="Out" Grid.Column="1" DataContext="{Binding BehaviorOptions}" Margin="2" />
<local:AngleBorder Direction="Left" Content="Layer:" Grid.Row="1" Margin="0,0,-2,0" />
<local:AngleComboBox Direction="Out" Grid.Row="1" Grid.Column="1" SelectedIndex="{Binding Layer}" ItemsSource="{Binding LayerOptions}" Margin="2" />

View File

@ -623,27 +623,18 @@
<StackPanel ToolTip="{Binding ParentName}">
<hsg3hv:CommonTableStreamControl />
<Grid Margin="20,2,2,2" Background="{DynamicResource Backlight}">
<Viewbox HorizontalAlignment="Right" Height="{Binding ActualHeight, ElementName=StreamTextBox}" Stretch="Uniform" IsHitTestVisible="False" Opacity=".5">
<ItemsControl ItemsSource="{Binding Visualizations}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<hsg3hv:PixelImage TransparentBrush="{DynamicResource Backlight}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Viewbox>
<TextBox Name="StreamTextBox" UndoLimit="0" Text="{Binding Content, UpdateSourceTrigger=PropertyChanged}"
Background="Transparent"
VerticalAlignment="Top"
CaretBrush="{DynamicResource Secondary}"
AcceptsReturn="True" FontFamily="Consolas"
Visibility="{Binding ShowContent, Converter={StaticResource BoolToVisibility}}">
<TextBox.InputBindings>
<KeyBinding Modifiers="Ctrl" Key="Z" Command="{Binding Undo}"/>
<KeyBinding Modifiers="Ctrl" Key="Y" Command="{Binding Redo}"/>
</TextBox.InputBindings>
</TextBox>
<hsg3hv:AutocompleteOverlay Target="{Binding ElementName=StreamTextBox}"/>
<Viewbox HorizontalAlignment="Right" Height="{Binding ActualHeight, ElementName=StreamTextBox}" Stretch="Uniform" IsHitTestVisible="False" Opacity=".5">
<ItemsControl ItemsSource="{Binding Visualizations}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<hsg3hv:PixelImage TransparentBrush="{DynamicResource Backlight}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Viewbox>
<hsg3hv:TextEditor x:Name="StreamTextBox" DataContext="{Binding ElementContent}"
Background="Transparent" VerticalAlignment="Top" Visibility="{Binding ShowContent, Converter={StaticResource BoolToVisibility}}" />
<hsg3hv:AutocompleteOverlay Target="{Binding ElementName=StreamTextBox}"/>
</Grid>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,2,0,0">
<hsg3hv:AngleButton Command="{Binding SetDefaultMoves}" Margin="0,0,4,0" Content="Default Moves" Direction="Out">

View File

@ -2,7 +2,7 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:HavenSoft.HexManiac.WPF.Controls">
<Grid Background="{DynamicResource Backlight}" TextBlock.FontFamily="Consolas" ClipToBounds="True">
<Grid Name="Body" TextBlock.FontFamily="Consolas" ClipToBounds="True" Background="Transparent">
<Canvas ClipToBounds="False" Width="0" HorizontalAlignment="Left">
<TextBlock Name="BasicLayer" Foreground="{DynamicResource Primary}" Margin="2" Text="{Binding PlainContent}">
<TextBlock.RenderTransform>

View File

@ -1330,6 +1330,7 @@
<!-- Text Editor Style -->
<Style TargetType="hsc:TextEditor">
<Setter Property="Background" Value="{DynamicResource Backlight}" />
<Setter Property="ContextMenuOverride">
<Setter.Value>
<ContextMenu>