- using System.Collections.Generic;
- using System.Text;
- namespace Ink.Runtime
- {
- /// <summary>
- /// The underlying type for a list item in ink. It stores the original list definition
- /// name as well as the item name, but without the value of the item. When the value is
- /// stored, it's stored in a KeyValuePair of InkListItem and int.
- /// </summary>
- public struct InkListItem
- {
- /// <summary>
- /// The name of the list where the item was originally defined.
- /// </summary>
- public readonly string originName;
- /// <summary>
- /// The main name of the item as defined in ink.
- /// </summary>
- public readonly string itemName;
- /// <summary>
- /// Create an item with the given original list definition name, and the name of this
- /// item.
- /// </summary>
- public InkListItem (string originName, string itemName)
- {
- this.originName = originName;
- this.itemName = itemName;
- }
- /// <summary>
- /// Create an item from a dot-separted string of the form "listDefinitionName.listItemName".
- /// </summary>
- public InkListItem (string fullName)
- {
- var nameParts = fullName.Split ('.');
- this.originName = nameParts [0];
- this.itemName = nameParts [1];
- }
- public static InkListItem Null {
- get {
- return new InkListItem (null, null);
- }
- }
- public bool isNull {
- get {
- return originName == null && itemName == null;
- }
- }
- /// <summary>
- /// Get the full dot-separated name of the item, in the form "listDefinitionName.itemName".
- /// </summary>
- public string fullName {
- get {
- return (originName ?? "?") + "." + itemName;
- }
- }
- /// <summary>
- /// Get the full dot-separated name of the item, in the form "listDefinitionName.itemName".
- /// Calls fullName internally.
- /// </summary>
- public override string ToString ()
- {
- return fullName;
- }
- /// <summary>
- /// Is this item the same as another item?
- /// </summary>
- public override bool Equals (object obj)
- {
- if (obj is InkListItem) {
- var otherItem = (InkListItem)obj;
- return otherItem.itemName == itemName
- && otherItem.originName == originName;
- }
- return false;
- }
- /// <summary>
- /// Get the hashcode for an item.
- /// </summary>
- public override int GetHashCode ()
- {
- int originCode = 0;
- int itemCode = itemName.GetHashCode ();
- if (originName != null)
- originCode = originName.GetHashCode ();
-
- return originCode + itemCode;
- }
- }
- /// <summary>
- /// The InkList is the underlying type that's used to store an instance of a
- /// list in ink. It's not used for the *definition* of the list, but for a list
- /// value that's stored in a variable.
- /// Somewhat confusingly, it's backed by a C# Dictionary, and has nothing to
- /// do with a C# List!
- /// </summary>
- public class InkList : Dictionary<InkListItem, int>
- {
- /// <summary>
- /// Create a new empty ink list.
- /// </summary>
- public InkList () { }
- /// <summary>
- /// Create a new ink list that contains the same contents as another list.
- /// </summary>
- public InkList(InkList otherList) : base(otherList)
- {
- var otherOriginNames = otherList.originNames;
- if( otherOriginNames != null )
- _originNames = new List<string>(otherOriginNames);
-
- if (otherList.origins != null)
- {
- origins = new List<ListDefinition>(otherList.origins);
- }
- }
- /// <summary>
- /// Create a new empty ink list that's intended to hold items from a particular origin
- /// list definition. The origin Story is needed in order to be able to look up that definition.
- /// </summary>
- public InkList (string singleOriginListName, Story originStory)
- {
- SetInitialOriginName (singleOriginListName);
- ListDefinition def;
- if (originStory.listDefinitions.TryListGetDefinition (singleOriginListName, out def))
- origins = new List<ListDefinition> { def };
- else
- throw new System.Exception ("InkList origin could not be found in story when constructing new list: " + singleOriginListName);
- }
- public InkList (KeyValuePair<InkListItem, int> singleElement)
- {
- Add (singleElement.Key, singleElement.Value);
- }
- /// <summary>
- /// Converts a string to an ink list and returns for use in the story.
- /// </summary>
- /// <returns>InkList created from string list item</returns>
- /// <param name="itemKey">Item key.</param>
- /// <param name="originStory">Origin story.</param>
- public static InkList FromString(string myListItem, Story originStory) {
- var listValue = originStory.listDefinitions.FindSingleItemListWithName (myListItem);
- if (listValue)
- return new InkList (listValue.value);
- else
- throw new System.Exception ("Could not find the InkListItem from the string '" + myListItem + "' to create an InkList because it doesn't exist in the original list definition in ink.");
- }
- /// <summary>
- /// Adds the given item to the ink list. Note that the item must come from a list definition that
- /// is already "known" to this list, so that the item's value can be looked up. By "known", we mean
- /// that it already has items in it from that source, or it did at one point - it can't be a
- /// completely fresh empty list, or a list that only contains items from a different list definition.
- /// </summary>
- public void AddItem (InkListItem item)
- {
- if (item.originName == null) {
- AddItem (item.itemName);
- return;
- }
-
- foreach (var origin in origins) {
- if (origin.name == item.originName) {
- int intVal;
- if (origin.TryGetValueForItem (item, out intVal)) {
- this [item] = intVal;
- return;
- } else {
- throw new System.Exception ("Could not add the item " + item + " to this list because it doesn't exist in the original list definition in ink.");
- }
- }
- }
- throw new System.Exception ("Failed to add item to list because the item was from a new list definition that wasn't previously known to this list. Only items from previously known lists can be used, so that the int value can be found.");
- }
- /// <summary>
- /// Adds the given item to the ink list, attempting to find the origin list definition that it belongs to.
- /// The item must therefore come from a list definition that is already "known" to this list, so that the
- /// item's value can be looked up. By "known", we mean that it already has items in it from that source, or
- /// it did at one point - it can't be a completely fresh empty list, or a list that only contains items from
- /// a different list definition.
- /// </summary>
- public void AddItem (string itemName)
- {
- ListDefinition foundListDef = null;
- foreach (var origin in origins) {
- if (origin.ContainsItemWithName (itemName)) {
- if (foundListDef != null) {
- throw new System.Exception ("Could not add the item " + itemName + " to this list because it could come from either " + origin.name + " or " + foundListDef.name);
- } else {
- foundListDef = origin;
- }
- }
- }
- if (foundListDef == null)
- throw new System.Exception ("Could not add the item " + itemName + " to this list because it isn't known to any list definitions previously associated with this list.");
- var item = new InkListItem (foundListDef.name, itemName);
- var itemVal = foundListDef.ValueForItem(item);
- this [item] = itemVal;
- }
- /// <summary>
- /// Returns true if this ink list contains an item with the given short name
- /// (ignoring the original list where it was defined).
- /// </summary>
- public bool ContainsItemNamed (string itemName)
- {
- foreach (var itemWithValue in this) {
- if (itemWithValue.Key.itemName == itemName) return true;
- }
- return false;
- }
- // Story has to set this so that the value knows its origin,
- // necessary for certain operations (e.g. interacting with ints).
- // Only the story has access to the full set of lists, so that
- // the origin can be resolved from the originListName.
- public List<ListDefinition> origins;
- public ListDefinition originOfMaxItem {
- get {
- if (origins == null) return null;
- var maxOriginName = maxItem.Key.originName;
- foreach (var origin in origins) {
- if (origin.name == maxOriginName)
- return origin;
- }
- return null;
- }
- }
- // Origin name needs to be serialised when content is empty,
- // assuming a name is availble, for list definitions with variable
- // that is currently empty.
- public List<string> originNames {
- get {
- if (this.Count > 0) {
- if (_originNames == null && this.Count > 0)
- _originNames = new List<string> ();
- else
- _originNames.Clear ();
- foreach (var itemAndValue in this)
- _originNames.Add (itemAndValue.Key.originName);
- }
- return _originNames;
- }
- }
- List<string> _originNames;
- public void SetInitialOriginName (string initialOriginName)
- {
- _originNames = new List<string> { initialOriginName };
- }
- public void SetInitialOriginNames (List<string> initialOriginNames)
- {
- if (initialOriginNames == null)
- _originNames = null;
- else
- _originNames = new List<string>(initialOriginNames);
- }
- /// <summary>
- /// Get the maximum item in the list, equivalent to calling LIST_MAX(list) in ink.
- /// </summary>
- public KeyValuePair<InkListItem, int> maxItem {
- get {
- KeyValuePair<InkListItem, int> max = new KeyValuePair<InkListItem, int>();
- foreach (var kv in this) {
- if (max.Key.isNull || kv.Value > max.Value)
- max = kv;
- }
- return max;
- }
- }
- /// <summary>
- /// Get the minimum item in the list, equivalent to calling LIST_MIN(list) in ink.
- /// </summary>
- public KeyValuePair<InkListItem, int> minItem {
- get {
- var min = new KeyValuePair<InkListItem, int> ();
- foreach (var kv in this) {
- if (min.Key.isNull || kv.Value < min.Value)
- min = kv;
- }
- return min;
- }
- }
- /// <summary>
- /// The inverse of the list, equivalent to calling LIST_INVERSE(list) in ink
- /// </summary>
- public InkList inverse {
- get {
- var list = new InkList ();
- if (origins != null) {
- foreach (var origin in origins) {
- foreach (var itemAndValue in origin.items) {
- if (!this.ContainsKey (itemAndValue.Key))
- list.Add (itemAndValue.Key, itemAndValue.Value);
- }
- }
- }
- return list;
- }
- }
- /// <summary>
- /// The list of all items from the original list definition, equivalent to calling
- /// LIST_ALL(list) in ink.
- /// </summary>
- public InkList all {
- get {
- var list = new InkList ();
- if (origins != null) {
- foreach (var origin in origins) {
- foreach (var itemAndValue in origin.items)
- list[itemAndValue.Key] = itemAndValue.Value;
- }
- }
- return list;
- }
- }
- /// <summary>
- /// Returns a new list that is the combination of the current list and one that's
- /// passed in. Equivalent to calling (list1 + list2) in ink.
- /// </summary>
- public InkList Union (InkList otherList)
- {
- var union = new InkList (this);
- foreach (var kv in otherList) {
- union [kv.Key] = kv.Value;
- }
- return union;
- }
- /// <summary>
- /// Returns a new list that is the intersection of the current list with another
- /// list that's passed in - i.e. a list of the items that are shared between the
- /// two other lists. Equivalent to calling (list1 ^ list2) in ink.
- /// </summary>
- public InkList Intersect (InkList otherList)
- {
- var intersection = new InkList ();
- foreach (var kv in this) {
- if (otherList.ContainsKey (kv.Key))
- intersection.Add (kv.Key, kv.Value);
- }
- return intersection;
- }
- /// <summary>
- /// Fast test for the existence of any intersection between the current list and another
- /// </summary>
- public bool HasIntersection(InkList otherList)
- {
- foreach (var kv in this)
- {
- if (otherList.ContainsKey(kv.Key))
- return true;
- }
- return false;
- }
- /// <summary>
- /// Returns a new list that's the same as the current one, except with the given items
- /// removed that are in the passed in list. Equivalent to calling (list1 - list2) in ink.
- /// </summary>
- /// <param name="listToRemove">List to remove.</param>
- public InkList Without (InkList listToRemove)
- {
- var result = new InkList (this);
- foreach (var kv in listToRemove)
- result.Remove (kv.Key);
- return result;
- }
- /// <summary>
- /// Returns true if the current list contains all the items that are in the list that
- /// is passed in. Equivalent to calling (list1 ? list2) in ink.
- /// </summary>
- /// <param name="otherList">Other list.</param>
- public bool Contains (InkList otherList)
- {
- if( otherList.Count == 0 || this.Count == 0 ) return false;
- foreach (var kv in otherList) {
- if (!this.ContainsKey (kv.Key)) return false;
- }
- return true;
- }
- /// <summary>
- /// Returns true if the current list contains an item matching the given name.
- /// </summary>
- /// <param name="otherList">Other list.</param>
- public bool Contains(string listItemName)
- {
- foreach (var kv in this)
- {
- if (kv.Key.itemName == listItemName) return true;
- }
- return false;
- }
- /// <summary>
- /// Returns true if all the item values in the current list are greater than all the
- /// item values in the passed in list. Equivalent to calling (list1 > list2) in ink.
- /// </summary>
- public bool GreaterThan (InkList otherList)
- {
- if (Count == 0) return false;
- if (otherList.Count == 0) return true;
- // All greater
- return minItem.Value > otherList.maxItem.Value;
- }
- /// <summary>
- /// Returns true if the item values in the current list overlap or are all greater than
- /// the item values in the passed in list. None of the item values in the current list must
- /// fall below the item values in the passed in list. Equivalent to (list1 >= list2) in ink,
- /// or LIST_MIN(list1) >= LIST_MIN(list2) && LIST_MAX(list1) >= LIST_MAX(list2).
- /// </summary>
- public bool GreaterThanOrEquals (InkList otherList)
- {
- if (Count == 0) return false;
- if (otherList.Count == 0) return true;
- return minItem.Value >= otherList.minItem.Value
- && maxItem.Value >= otherList.maxItem.Value;
- }
- /// <summary>
- /// Returns true if all the item values in the current list are less than all the
- /// item values in the passed in list. Equivalent to calling (list1 < list2) in ink.
- /// </summary>
- public bool LessThan (InkList otherList)
- {
- if (otherList.Count == 0) return false;
- if (Count == 0) return true;
- return maxItem.Value < otherList.minItem.Value;
- }
- /// <summary>
- /// Returns true if the item values in the current list overlap or are all less than
- /// the item values in the passed in list. None of the item values in the current list must
- /// go above the item values in the passed in list. Equivalent to (list1 <= list2) in ink,
- /// or LIST_MAX(list1) <= LIST_MAX(list2) && LIST_MIN(list1) <= LIST_MIN(list2).
- /// </summary>
- public bool LessThanOrEquals (InkList otherList)
- {
- if (otherList.Count == 0) return false;
- if (Count == 0) return true;
- return maxItem.Value <= otherList.maxItem.Value
- && minItem.Value <= otherList.minItem.Value;
- }
- public InkList MaxAsList ()
- {
- if (Count > 0)
- return new InkList (maxItem);
- else
- return new InkList ();
- }
- public InkList MinAsList ()
- {
- if (Count > 0)
- return new InkList (minItem);
- else
- return new InkList ();
- }
- /// <summary>
- /// Returns a sublist with the elements given the minimum and maxmimum bounds.
- /// The bounds can either be ints which are indices into the entire (sorted) list,
- /// or they can be InkLists themselves. These are intended to be single-item lists so
- /// you can specify the upper and lower bounds. If you pass in multi-item lists, it'll
- /// use the minimum and maximum items in those lists respectively.
- /// WARNING: Calling this method requires a full sort of all the elements in the list.
- /// </summary>
- public InkList ListWithSubRange(object minBound, object maxBound)
- {
- if (this.Count == 0) return new InkList();
- var ordered = orderedItems;
- int minValue = 0;
- int maxValue = int.MaxValue;
- if (minBound is int)
- {
- minValue = (int)minBound;
- }
- else
- {
- if( minBound is InkList && ((InkList)minBound).Count > 0 )
- minValue = ((InkList)minBound).minItem.Value;
- }
- if (maxBound is int)
- maxValue = (int)maxBound;
- else
- {
- if (minBound is InkList && ((InkList)minBound).Count > 0)
- maxValue = ((InkList)maxBound).maxItem.Value;
- }
- var subList = new InkList();
- subList.SetInitialOriginNames(originNames);
- foreach(var item in ordered) {
- if( item.Value >= minValue && item.Value <= maxValue ) {
- subList.Add(item.Key, item.Value);
- }
- }
- return subList;
- }
- /// <summary>
- /// Returns true if the passed object is also an ink list that contains
- /// the same items as the current list, false otherwise.
- /// </summary>
- public override bool Equals (object other)
- {
- var otherRawList = other as InkList;
- if (otherRawList == null) return false;
- if (otherRawList.Count != Count) return false;
- foreach (var kv in this) {
- if (!otherRawList.ContainsKey (kv.Key))
- return false;
- }
- return true;
- }
- /// <summary>
- /// Return the hashcode for this object, used for comparisons and inserting into dictionaries.
- /// </summary>
- public override int GetHashCode ()
- {
- int ownHash = 0;
- foreach (var kv in this)
- ownHash += kv.Key.GetHashCode ();
- return ownHash;
- }
- List<KeyValuePair<InkListItem, int>> orderedItems {
- get {
- var ordered = new List<KeyValuePair<InkListItem, int>>();
- ordered.AddRange(this);
- ordered.Sort((x, y) => {
- // Ensure consistent ordering of mixed lists.
- if( x.Value == y.Value ) {
- return x.Key.originName.CompareTo(y.Key.originName);
- } else {
- return x.Value.CompareTo(y.Value);
- }
- });
- return ordered;
- }
- }
- /// <summary>
- /// Returns a string in the form "a, b, c" with the names of the items in the list, without
- /// the origin list definition names. Equivalent to writing {list} in ink.
- /// </summary>
- public override string ToString ()
- {
- var ordered = orderedItems;
- var sb = new StringBuilder ();
- for (int i = 0; i < ordered.Count; i++) {
- if (i > 0)
- sb.Append (", ");
- var item = ordered [i].Key;
- sb.Append (item.itemName);
- }
- return sb.ToString ();
- }
- }
- }