HexManiacAdvance/src/HexManiac.Core/Models/ModelTable.cs
haven1433 76f1977fb8 fix python enums-with-spaces parsing
* don't include " within the string when getting the value
* use `TryParse` instead of `Write` for converting the enum value from a string, since this is an all-to-one edit, not a pull-from-stream edit.
2022-12-11 22:11:27 -06:00

517 lines
24 KiB
C#

using HavenSoft.HexManiac.Core.Models.Runs;
using HavenSoft.HexManiac.Core.Models.Runs.Sprites;
using HavenSoft.HexManiac.Core.ViewModels.DataFormats;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Dynamic;
using System.Linq;
using System.Text;
namespace HavenSoft.HexManiac.Core.Models {
public class ModelTable : DynamicObject, IReadOnlyList<ModelArrayElement> {
private readonly IDataModel model;
private readonly int arrayAddress;
private readonly Func<ModelDelta> tokenFactory;
private readonly ITableRun run;
public int Count => run?.ElementCount ?? 0;
public int __len__() => Count; // for python
public ITableRun Run => run;
public ModelArrayElement this[int value] {
get {
return new ModelArrayElement(model, arrayAddress, value, tokenFactory, run);
}
}
public ModelArrayElement this[string value] {
get {
var table = run;
if (ArrayRunEnumSegment.TryMatch(value, table.ElementNames, out int index)) {
return this[index];
} else {
throw new NotImplementedException();
}
}
}
public ModelTable(IDataModel model, ITableRun table, Func<ModelDelta> tokenFactory = null) : this(model, table.Start, tokenFactory, table) { }
public ModelTable(IDataModel model, int address, Func<ModelDelta> tokenFactory = null, ITableRun tableRun = null) {
(this.model, arrayAddress) = (model, address);
run = tableRun ?? model.GetNextRun(address) as ITableRun;
this.tokenFactory = tokenFactory ?? (() => new NoDataChangeDeltaModel());
}
public override bool TryGetMember(GetMemberBinder binder, out object? result) {
result = this[binder.Name];
return true;
}
public IEnumerator<ModelArrayElement> GetEnumerator() {
var count = Count;
for (int i = 0; i < count; i++) yield return this[i];
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
public class ModelArrayElement : DynamicObject {
private readonly IDataModel model;
private readonly int arrayAddress;
private readonly int arrayIndex;
private readonly ITableRun table;
private readonly Func<ModelDelta> tokenFactory;
public int Start => table.Start + table.ElementLength * arrayIndex;
public int ArrayIndex => arrayIndex;
public int Length => table.ElementLength;
public string Address => (table.Start + table.ElementLength * arrayIndex).ToAddress();
public ITableRun Table => table;
public IDataModel Model => model;
public ModelDelta Token => tokenFactory();
public ModelArrayElement(IDataModel model, int address, int index, Func<ModelDelta> tokenFactory, ITableRun table) {
(this.model, arrayAddress, arrayIndex) = (model, address, index);
this.table = table ?? (ITableRun)model.GetNextRun(arrayAddress);
this.tokenFactory = tokenFactory;
}
public bool HasField(string name) => table.ElementContent.Any(field => field.Name == name);
public string GetFieldName(int index) => table.ElementContent[index].Name;
public string GetStringValue(string fieldName) {
var elementOffset = table.ElementContent.Until(segment => segment.Name == fieldName).Sum(segment => segment.Length);
var valueAddress = table.Start + table.ElementLength * arrayIndex + elementOffset;
var seg = table.ElementContent.Single(segment => segment.Name == fieldName);
if (seg.Type == ElementContentType.PCS) {
return model.TextConverter.Convert(model, valueAddress, seg.Length).Trim('"');
} else if (seg.Type == ElementContentType.Pointer) {
valueAddress = model.ReadPointer(valueAddress);
if (valueAddress < 0 || valueAddress >= model.Count) return string.Empty;
var length = PCSString.ReadString(model, valueAddress, true);
if (length < 1) return string.Empty;
return model.TextConverter.Convert(model, valueAddress, length).Trim('"');
} else {
throw new NotImplementedException();
}
}
public string Serialize(string fieldName) {
var elementOffset = table.ElementContent.Until(segment => segment.Name == fieldName).Sum(segment => segment.Length);
var valueAddress = table.Start + table.ElementLength * arrayIndex + elementOffset;
var seg = table.ElementContent.Single(segment => segment.Name == fieldName);
if (seg.Type == ElementContentType.Pointer) {
var destination = model.ReadPointer(valueAddress);
if (destination == Pointer.NULL) return "null";
var run = model.GetNextRun(destination);
if (run is ArrayRun tRun) {
run = new TableStreamRun(model, run.Start, run.PointerSources, run.FormatString,
tRun.ElementContent, new FixedLengthStreamStrategy(tRun.ElementCount));
}
if (run is IStreamRun sRun) return sRun.SerializeRun();
return destination.ToAddress();
}
if (seg.Type == ElementContentType.PCS) return GetStringValue(fieldName);
if (seg is ArrayRunEnumSegment) return GetEnumValue(fieldName);
return GetValue(fieldName).ToString();
}
public int[,] GetSprite(string fieldName) {
var elementOffset = table.ElementContent.Until(segment => segment.Name == fieldName).Sum(segment => segment.Length);
var valueAddress = table.Start + table.ElementLength * arrayIndex + elementOffset;
var seg = table.ElementContent.Single(segment => segment.Name == fieldName);
if (seg is ArrayRunPointerSegment pointerSeg) {
var destination = model.ReadPointer(valueAddress);
if (model.GetNextRun(destination) is not ISpriteRun spriteRun) {
IFormattedRun tempRun = new NoInfoRun(destination, new SortedSpan<int>(valueAddress));
model.FormatRunFactory.GetStrategy(pointerSeg.InnerFormat).TryParseData(model, string.Empty, destination, ref tempRun);
spriteRun = tempRun as ISpriteRun;
if (spriteRun == null) return null;
}
return spriteRun.GetPixels(model, 0, arrayIndex);
} else {
throw new NotImplementedException();
}
}
public IReadOnlyList<short> GetPalette(string fieldName) {
var elementOffset = table.ElementContent.Until(segment => segment.Name == fieldName).Sum(segment => segment.Length);
var valueAddress = table.Start + table.ElementLength * arrayIndex + elementOffset;
var seg = table.ElementContent.Single(segment => segment.Name == fieldName);
if (seg is ArrayRunPointerSegment pointerSeg) {
var destination = model.ReadPointer(valueAddress);
if (model.GetNextRun(destination) is not IPaletteRun palRun) {
IFormattedRun tempRun = new NoInfoRun(destination, new SortedSpan<int>(valueAddress));
model.FormatRunFactory.GetStrategy(pointerSeg.InnerFormat).TryParseData(model, string.Empty, destination, ref tempRun);
palRun = tempRun as IPaletteRun;
if (palRun == null) return null;
}
return palRun.GetPalette(model, 0);
} else {
throw new NotImplementedException();
}
}
public int GetValue(string fieldName) {
var elementOffset = table.ElementContent.Until(segment => segment.Name == fieldName).Sum(segment => segment.Length);
var valueAddress = table.Start + table.ElementLength * arrayIndex + elementOffset;
var seg = table.ElementContent.Single(segment => segment.Name == fieldName);
if (seg.Type == ElementContentType.Integer) {
return model.ReadMultiByteValue(valueAddress, seg.Length);
} else {
return model.ReadMultiByteValue(valueAddress, seg.Length.LimitToRange(0, 3));
}
}
public int GetValue(int fieldIndex) {
var elementOffset = table.ElementContent.Take(fieldIndex).Sum(segment => segment.Length);
var valueAddress = table.Start + table.ElementLength * arrayIndex + elementOffset;
var seg = table.ElementContent[fieldIndex];
if (seg.Type == ElementContentType.Integer) {
return model.ReadMultiByteValue(valueAddress, seg.Length);
} else {
throw new NotImplementedException();
}
}
public bool TryGetValue(string fieldName, out int value) {
value = -1;
if (!HasField(fieldName)) return false;
value = GetValue(fieldName);
return true;
}
public int GetAddress(string fieldName) {
var elementOffset = table.ElementContent.Until(segment => segment.Name == fieldName).Sum(segment => segment.Length);
var valueAddress = table.Start + table.ElementLength * arrayIndex + elementOffset;
var seg = table.ElementContent.FirstOrDefault(segment => segment.Name == fieldName);
if (seg == null) return Pointer.NULL;
if (seg.Type == ElementContentType.Pointer) {
return model.ReadPointer(valueAddress);
} else if (seg.Type == ElementContentType.Integer && seg.Length == 4) {
return model.ReadPointer(valueAddress); // user wants a pointer, read it like a pointer
} else {
throw new NotImplementedException();
}
}
public string GetEnumValue(string fieldName) {
var elementOffset = table.ElementContent.Until(segment => segment.Name == fieldName).Sum(segment => segment.Length);
var valueAddress = table.Start + table.ElementLength * arrayIndex + elementOffset;
var seg = table.ElementContent.Single(segment => segment.Name == fieldName);
if (seg is ArrayRunEnumSegment enumSeg) {
using (ModelCacheScope.CreateScope(model)) {
return enumSeg.ToText(model, valueAddress, false).Trim('"');
}
} else {
throw new NotImplementedException();
}
}
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);
}
public object this[string fieldName] {
get {
var seg = table.ElementContent.Single(segment => segment.Name == fieldName);
var segmentOffset = table.ElementContent.Until(s => s == seg).Sum(s => s.Length);
if (seg is ArrayRunEnumSegment) return GetEnumValue(fieldName);
if (seg.Type == ElementContentType.Pointer) {
var address = GetAddress(fieldName);
if (model.GetNextRun(address) is ITableRun table1) {
return new ModelTable(model, table1.Start, tokenFactory);
}
return address;
}
if (seg.Type == ElementContentType.PCS) return GetStringValue(fieldName);
if (seg is ArrayRunTupleSegment tuple) {
return new ModelTupleElement(model, table, arrayIndex, segmentOffset, tuple, tokenFactory);
}
if (seg is ArrayRunBitArraySegment bits) {
return new ModelTupleElement(model, table, arrayIndex, segmentOffset, bits, tokenFactory);
}
return GetValue(fieldName);
}
set {
var seg = table.ElementContent.Single(segment => segment.Name == fieldName);
if (seg is ArrayRunEnumSegment) SetEnumValue(fieldName, (string)value);
else if (seg.Type == ElementContentType.Pointer) {
if (value is string str) {
SetStringValue(fieldName, str);
} else {
SetAddress(fieldName, (int)value);
}
} else if (seg.Type == ElementContentType.PCS) SetStringValue(fieldName, (string)value);
else SetValue(fieldName, (int)value);
}
}
public void SetEnumValue(string fieldName, string valueText) {
var elementOffset = table.ElementContent.Until(segment => segment.Name == fieldName).Sum(segment => segment.Length);
var valueAddress = table.Start + table.ElementLength * arrayIndex + elementOffset;
var seg = table.ElementContent.Single(segment => segment.Name == fieldName);
if (seg is ArrayRunEnumSegment enumSeg) {
if (enumSeg.TryParse(model, valueText, out int value)) {
model.WriteMultiByteValue(valueAddress, seg.Length, tokenFactory(), value);
table.NotifyChildren(Model, tokenFactory(), arrayIndex, table.ElementContent.IndexOf(seg));
}
} else {
throw new NotImplementedException();
}
}
public void SetAddress(string fieldName, int value) {
var elementOffset = table.ElementContent.Until(segment => segment.Name == fieldName).Sum(segment => segment.Length);
var valueAddress = table.Start + table.ElementLength * arrayIndex + elementOffset;
var seg = table.ElementContent.Single(segment => segment.Name == fieldName);
if (seg.Type == ElementContentType.Pointer) {
model.UpdateArrayPointer(Token, seg, table.ElementContent, arrayIndex, valueAddress, value);
} else {
// it's not a pointer so don't update any formats to think that this points to them
// bet we should still update the value.
model.WritePointer(Token, valueAddress, value);
}
}
public void SetStringValue(string fieldName, string value) {
var elementOffset = table.ElementContent.Until(segment => segment.Name == fieldName).Sum(segment => segment.Length);
var valueAddress = table.Start + table.ElementLength * arrayIndex + elementOffset;
var seg = table.ElementContent.Single(segment => segment.Name == fieldName);
var token = tokenFactory();
if (seg.Type == ElementContentType.PCS) {
var bytes = model.TextConverter.Convert(value, out var _);
while (bytes.Count > seg.Length) bytes.RemoveAt(bytes.Count - 1);
bytes[bytes.Count - 1] = 0xFF;
while (bytes.Count < seg.Length) bytes.Add(0);
token.ChangeData(model, valueAddress, bytes);
} else if (seg.Type == ElementContentType.Pointer) {
valueAddress = model.ReadPointer(valueAddress);
var length = PCSString.ReadString(model, valueAddress, true);
var bytes = model.TextConverter.Convert(value, out var _);
var pcsRun = (PCSRun)model.GetNextRun(valueAddress);
pcsRun = model.RelocateForExpansion(token, pcsRun, bytes.Count);
pcsRun = (PCSRun)pcsRun.DeserializeRun(value, token, out var _, out var _);
model.ObserveRunWritten(token, pcsRun);
} else {
throw new NotImplementedException();
}
}
public void SetValue(string fieldName, int value) {
var elementOffset = table.ElementContent.Until(segment => segment.Name == fieldName).Sum(segment => segment.Length);
var valueAddress = table.Start + table.ElementLength * arrayIndex + elementOffset;
var seg = table.ElementContent.Single(segment => segment.Name == fieldName);
if (seg.Type == ElementContentType.Integer || seg.Type == ElementContentType.BitArray) {
model.WriteMultiByteValue(valueAddress, seg.Length, tokenFactory(), value);
table.NotifyChildren(Model, tokenFactory(), arrayIndex, table.ElementContent.IndexOf(seg));
} else {
throw new NotImplementedException();
}
}
public void SetValue(int fieldIndex, int value) {
var elementOffset = table.ElementContent.Take(fieldIndex).Sum(segment => segment.Length);
var valueAddress = table.Start + table.ElementLength * arrayIndex + elementOffset;
var seg = table.ElementContent[fieldIndex];
if (seg.Type == ElementContentType.Integer || seg.Type == ElementContentType.BitArray) {
model.WriteMultiByteValue(valueAddress, seg.Length, tokenFactory(), value);
} else {
throw new NotImplementedException();
}
}
public bool TryGetSubTable(string fieldName, out ModelTable table) {
table = GetSubTable(fieldName);
return table != null;
}
public ModelTable GetSubTable(string fieldName) {
var elementOffset = table.ElementContent.Until(segment => segment.Name == fieldName).Sum(segment => segment.Length);
var valueAddress = table.Start + table.ElementLength * arrayIndex + elementOffset;
if (valueAddress < 0 || valueAddress >= model.Count) return null;
var seg = table.ElementContent.Single(segment => segment.Name == fieldName);
if (seg is ArrayRunPointerSegment pointerSeg) {
var destination = model.ReadPointer(valueAddress);
if (destination < 0 || destination >= model.Count) return null;
var error = ArrayRun.TryParse(model, fieldName, pointerSeg.InnerFormat, destination, SortedSpan.One(valueAddress), table.ElementContent, out var childTable);
if (error.HasError) {
childTable = model.GetNextRun(destination) as ITableRun;
}
if (childTable == null) {
// one last try, with validation turned off
error = ArrayRun.TryParse(model, fieldName, pointerSeg.InnerFormat, destination, SortedSpan.One(valueAddress), table.ElementContent, false, out childTable);
if (error.HasError) return null;
}
return new ModelTable(model, destination, tokenFactory, childTable);
} else {
return null; // format didn't match expected, give up
}
}
#region DynamicObject
public override bool TryGetMember(GetMemberBinder binder, out object? result) {
result = null;
var seg = table.ElementContent.FirstOrDefault(segment => segment.Name == binder.Name);
if (seg == null) {
throw new ArgumentException($"Couldn't find a member named {binder.Name}. Available members include: {", ".Join(table.ElementContent.Select(s => s.Name))}");
}
result = this[seg.Name];
return true;
}
public override bool TrySetMember(SetMemberBinder binder, object? value) {
var seg = table.ElementContent.FirstOrDefault(segment => segment.Name == binder.Name);
if (seg == null) return base.TrySetMember(binder, value);
this[seg.Name] = value;
return true;
}
public override string ToString() {
var result = new StringBuilder("{ ");
bool first = true;
foreach (var seg in table.ElementContent) {
if (!first) result.Append(", ");
first = false;
result.Append(seg.Name + ": " + this[seg.Name]);
}
result.Append(" }");
return result.ToString();
}
#endregion
}
public class ModelTupleElement : DynamicObject {
private readonly IDataModel model;
private readonly ITableRun table;
private readonly int arrayIndex, segmentOffset;
private readonly ArrayRunTupleSegment tuple;
private readonly Func<ModelDelta> tokenFactory;
public ModelTupleElement(IDataModel model, ITableRun table, int arrayIndex, int segmentOffset, ArrayRunTupleSegment tuple, Func<ModelDelta> tokenFactory) {
this.model = model;
this.table = table;
this.arrayIndex = arrayIndex;
this.segmentOffset = segmentOffset;
this.tuple = tuple;
this.tokenFactory = tokenFactory;
}
public ModelTupleElement(IDataModel model, ITableRun table, int arrayIndex, int segmentOffset, ArrayRunBitArraySegment bits, Func<ModelDelta> tokenFactory) {
var contract = "|".Join(bits.GetOptions(model).Select(option => option + "."));
this.model = model;
this.table = table;
this.arrayIndex = arrayIndex;
this.segmentOffset = segmentOffset;
this.tuple = new ArrayRunTupleSegment(bits.Name, contract, bits.Length);
this.tokenFactory = tokenFactory;
}
public object this[string fieldName] {
get => GetValue(fieldName);
set {
if (value is bool b) value = Convert.ToInt32(b);
SetValue(fieldName, (int)value);
}
}
public bool HasField(string name) => tuple.Elements.Any(field => field.Name == name);
public int GetValue(string fieldName) {
var tup = tuple.Elements.Single(seg => seg.Name == fieldName);
var start = table.Start + table.ElementLength * arrayIndex;
start += table.ElementContent.Until(seg => seg == tuple).Sum(seg => seg.Length);
var bitOffset = tuple.Elements.Until(seg => seg == tup).Sum(seg => seg.BitWidth);
return tup.Read(model, start, bitOffset);
}
public void SetValue(string fieldName, int value) {
var tup = tuple.Elements.Single(seg => seg.Name == fieldName);
var start = table.Start + table.ElementLength * arrayIndex + segmentOffset;
var bitOffset = tuple.Elements.Until(seg => seg == tup).Sum(seg => seg.BitWidth);
tup.Write(model, tokenFactory(), start, bitOffset, value);
}
#region DynamicObject
public override bool TryGetMember(GetMemberBinder binder, out object? result) {
result = null;
var seg = tuple.Elements.FirstOrDefault(segment => segment.Name == binder.Name);
if (seg == null) {
throw new ArgumentException($"Couldn't find a member named {binder.Name}. Available members include: {", ".Join(table.ElementContent.Select(s => s.Name))}");
}
result = this[seg.Name];
return true;
}
public override bool TrySetMember(SetMemberBinder binder, object? value) {
var seg = tuple.Elements.FirstOrDefault(segment => segment.Name == binder.Name);
if (seg == null) return base.TrySetMember(binder, value);
this[seg.Name] = value;
return true;
}
public override string ToString() {
var result = new StringBuilder("{ ");
bool first = true;
foreach (var seg in tuple.Elements) {
if (!first) result.Append(", ");
first = false;
result.Append(seg.Name + ": " + this[seg.Name]);
}
result.Append(" }");
return result.ToString();
}
#endregion
}
public class AnchorGroup : DynamicObject {
private readonly IDataModel model;
private readonly Func<ModelDelta> tokenFactory;
private readonly string header;
public static IDictionary<string, AnchorGroup> GetTopLevelAnchorGroups(IDataModel model, Func<ModelDelta> tokenFactory) {
var topLevel = new HashSet<string>();
foreach (var anchor in model.Anchors) topLevel.Add(anchor.Split('.')[0]);
var results = new Dictionary<string, AnchorGroup>();
foreach (var top in topLevel) {
results.Add(top, new(model, tokenFactory, top));
}
return results;
}
public AnchorGroup(IDataModel model, Func<ModelDelta> tokenFactory, string header) {
this.model = model;
this.tokenFactory = tokenFactory;
this.header = header;
}
public override bool TryGetMember(GetMemberBinder binder, out object? result) {
var name = header + "." + binder.Name;
var address = model.GetAddressFromAnchor(tokenFactory(), -1, name);
if (address < 0) {
result = new AnchorGroup(model, tokenFactory, name);
return true;
}
var run = model.GetNextRun(address);
if (run is ITableRun table) {
result = new ModelTable(model, table, tokenFactory);
return true;
}
// TODO not a table, but maybe something else (sprite, constant, text, etc)
return base.TryGetMember(binder, out result);
}
}
}