using System; using System.Collections.Generic; using System.IO; using Vellum; namespace Ariadne.Vellum { /// /// Holds everything an Arc saves as SaveNodes, and writes it as one Lua table. /// public class ArcRecord : IArcWriter, IArcReader { public const string CursorKey = "cursor"; public const string FinishedKey = "finished"; public const string FieldsKey = "fields"; public const string LocalsKey = "locals"; private readonly SaveSession session; private readonly SaveReader fieldReader; private readonly SaveReader localReader; /// /// Gets the labels the arc is paused at, outermost passage first; empty before its first tick. /// public List Cursor { get; } = new List(); IReadOnlyList IArcReader.Cursor { get { return Cursor; } } public bool Finished { get; set; } /// /// The saved fields, a table keyed by field name. /// public readonly SaveNode Fields; /// /// The saved locals and passage parameters keyed by "Passage.local", and a table for each call the arc was /// paused in, keyed by "Passage.label/", holding that call's own. /// public readonly SaveNode Locals; /// /// Ends the key of a call's table, which no local's key does. /// public const char ScopeMark = '/'; // The table the record was read from, which an arc is published against. Null for a record being written. private readonly SaveNode node; private ArcRecord(SaveSession session, SaveNode node, SaveNode fields, SaveNode locals) { this.session = session; this.node = node; Fields = fields; Locals = locals; fieldReader = new SaveReader(fields, session); localReader = new SaveReader(locals, session); } /// Saves the types Vellum doesn't know, eg: those the arc's assembly names. public ArcRecord(SaveSession session) : this(session, node: null, fields: SaveNode.NewTable(), locals: SaveNode.NewTable()) { } /// /// Holds the key of the list of type arguments a call's table may have, which no local's key is, since those /// all have a dot. /// public const string TypesKey = "types"; private static IEnumerable KeysOf(SaveNode table, bool scopes) { foreach (string key in table.Entries.Keys) { if (key.EndsWith(ScopeMark.ToString(), StringComparison.Ordinal) == scopes && (scopes || key != TypesKey)) { yield return key; } } } private void WriteTypes(SaveNode table, Type[] types) { SaveNode names = SaveNode.NewList(); foreach (Type type in types) { string name; if (!session.TryNameOf(type, out name)) { throw new NotSupportedException("A " + type.FullName + " can't be saved as a type argument, " + "since it, or a type it's made from, isn't one a registered converter saves by name."); } names.Items.Add(SaveNode.OfText(name)); } table.Entries[TypesKey] = names; } private bool TryReadTypes(SaveNode table, out Type[] types) { SaveNode names; if (!table.Entries.TryGetValue(TypesKey, out names)) { types = null; return false; } session.NoteRead(table, TypesKey); IReadOnlyList items = names.RequireItems(); types = new Type[items.Count]; for (int i = 0; i < items.Count; ++i) { types[i] = session.ResolveName(items[i].RequireText()); } return true; } public IEnumerable LocalKeys { get { return KeysOf(Locals, scopes: false); } } public IEnumerable ScopeNames { get { return KeysOf(Locals, scopes: true); } } /// /// Holds one call's table of locals, which is written and read through as the arc itself is. /// private sealed class LocalScope : IArcWriter, IArcReader { private readonly ArcRecord record; private readonly SaveNode table; private readonly SaveReader reader; public LocalScope(ArcRecord record, SaveNode table) { this.record = record; this.table = table; reader = new SaveReader(table, record.session); } void IArcWriter.Cursor(IReadOnlyList cursor) { ((IArcWriter)record).Cursor(cursor); } void IArcWriter.Finished(bool finished) { ((IArcWriter)record).Finished(finished); } public void Field(string key, T value) { record.Field(key, value); } public void Local(string key, T value) { table.Entries.Add(key, record.session.ToNode(value)); } public IArcWriter Scope(string name) { return record.NewScope(table, name); } public IReadOnlyList Cursor { get { return record.Cursor; } } public bool Finished { get { return record.Finished; } } public IEnumerable LocalKeys { get { return KeysOf(table, scopes: false); } } public IEnumerable ScopeNames { get { return KeysOf(table, scopes: true); } } public bool TryField(string key, out T value) { return record.TryField(key, out value); } public bool HasField(string key) { return record.HasField(key); } public bool TryLocal(string key, out T value) { return reader.TryRead(key, out value); } public bool TryScope(string name, out IArcReader scope) { return record.TryScopeIn(table, name, out scope); } public void TypeArguments(params Type[] types) { record.WriteTypes(table, types); } public bool TryTypeArguments(out Type[] types) { return record.TryReadTypes(table, out types); } public void Publish(object built) { record.Publish(built); } } private IArcWriter NewScope(SaveNode parent, string name) { SaveNode table = SaveNode.NewTable(); parent.Entries.Add(name, table); return new LocalScope(this, table); } private bool TryScopeIn(SaveNode parent, string name, out IArcReader scope) { SaveNode table; if (parent.Entries.TryGetValue(name, out table) && table.Kind == SaveNode.Kinds.Table) { session.NoteRead(parent, name); scope = new LocalScope(this, table); return true; } scope = null; return false; } /// The arc already wrote a call with this name. public IArcWriter Scope(string name) { return NewScope(Locals, name); } public bool TryScope(string name, out IArcReader scope) { return TryScopeIn(Locals, name, out scope); } /// A type can't be named in a save. public void TypeArguments(params Type[] types) { WriteTypes(Locals, types); } public bool TryTypeArguments(out Type[] types) { return TryReadTypes(Locals, out types); } void IArcWriter.Cursor(IReadOnlyList cursor) { Cursor.Clear(); Cursor.AddRange(cursor); } void IArcWriter.Finished(bool finished) { Finished = finished; } /// The arc already wrote a field with this key. /// A can't be saved. public void Field(string key, T value) { Fields.Entries.Add(key, session.ToNode(value)); } /// The arc already wrote a local with this key. /// A can't be saved. public void Local(string key, T value) { Locals.Entries.Add(key, session.ToNode(value)); } public bool TryField(string key, out T value) { return fieldReader.TryRead(key, out value); } public bool HasField(string key) { return Fields.Entries.ContainsKey(key); } public bool TryLocal(string key, out T value) { return localReader.TryRead(key, out value); } /// The record is being written, not read. /// is null. public void Publish(object built) { if (node == null) { throw new InvalidOperationException("A record being written has no saved arc to publish."); } session.Publish(node, built); } private SaveNode CursorNode() { SaveNode returnMe = SaveNode.NewList(); foreach (string label in Cursor) { returnMe.Items.Add(SaveNode.OfText(label)); } return returnMe; } /// /// Returns the table Save writes, which holds this record's own Fields and Locals rather than copies. /// /// A cursor label is null. public SaveNode ToNode() { SaveNode returnMe = SaveNode.NewTable(); returnMe.Entries[CursorKey] = CursorNode(); returnMe.Entries[FinishedKey] = SavedValue.ToNode(Finished, typeof(bool)); returnMe.Entries[FieldsKey] = Fields; returnMe.Entries[LocalsKey] = Locals; return returnMe; } private static InvalidDataException Malformed(string message) { return new InvalidDataException("Malformed save: " + message); } private static SaveNode TableOrEmpty(SaveNode record, string key) { SaveNode returnMe; if (!record.Entries.TryGetValue(key, out returnMe)) { return SaveNode.NewTable(); } if (returnMe.Kind != SaveNode.Kinds.Table) { throw Malformed(key + " must be a table."); } return returnMe; } /// /// Reads a record from the table ToNode made. Missing entries keep a fresh record's values. /// /// An entry the record has is the wrong shape. public static ArcRecord FromNode(SaveNode node, SaveSession session) { if (node.Kind != SaveNode.Kinds.Table) { throw Malformed("an arc must be a table."); } var returnMe = new ArcRecord(session, node, fields: TableOrEmpty(node, FieldsKey), locals: TableOrEmpty(node, LocalsKey)); SaveNode cursor; if (node.Entries.TryGetValue(CursorKey, out cursor)) { foreach (SaveNode label in cursor.RequireItems()) { returnMe.Cursor.Add(label.RequireText()); } } var reader = new SaveReader(node, session); reader.Skip(CursorKey); reader.Skip(FieldsKey); reader.Skip(LocalsKey); bool finished; if (reader.TryRead(FinishedKey, out finished)) { returnMe.Finished = finished; } return returnMe; } } }