Files
Mementos/Unity/Alternate Genre Jam/Assets/Ink/InkLibs/InkRuntime/InkList.cs
2021-06-30 21:39:19 +10:00

585 lines
21 KiB
C#

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)
{
_originNames = otherList.originNames;
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>
/// 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)
{
foreach (var kv in otherList) {
if (!this.ContainsKey (kv.Key)) return false;
}
return true;
}
/// <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) &amp;&amp; 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 &lt; 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 &lt;= list2) in ink,
/// or LIST_MAX(list1) &lt;= LIST_MAX(list2) &amp;&amp; LIST_MIN(list1) &lt;= 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 ();
}
}
}