diff --git a/Nuclex.Support (mono-3.5-unity).csproj b/Nuclex.Support (mono-3.5-unity).csproj
index 1575366..12d2d99 100644
--- a/Nuclex.Support (mono-3.5-unity).csproj
+++ b/Nuclex.Support (mono-3.5-unity).csproj
@@ -38,6 +38,16 @@
+
+
+ ExpressionTreeCloner.cs
+
+
+ ExpressionTreeCloner.cs
+
+
+ ExpressionTreeCloner.cs
+
@@ -101,6 +111,10 @@
ObservableList.cs
+
+
+ ObservableSet.cs
+ PairPriorityQueue.cs
@@ -137,6 +151,10 @@
ReadOnlyList.cs
+
+
+ ReadOnlySet.cs
+ ReverseComparer.cs
@@ -148,6 +166,10 @@
TransformingReadOnlyCollection.cs
+
+
+ TransformingReadOnlyCollection.cs
+ WeakCollection.cs
@@ -185,6 +207,10 @@
CommandLine.cs
+
+
+ ParserHelper.cs
+ PropertyChangedEventArgsHelper.cs
@@ -253,6 +279,11 @@
XmlHelper.cs
+
+
+ Foundation.snk
+
+
diff --git a/Nuclex.Support (net-4.0).csproj b/Nuclex.Support (net-4.0).csproj
index 4fe8546..e01352e 100644
--- a/Nuclex.Support (net-4.0).csproj
+++ b/Nuclex.Support (net-4.0).csproj
@@ -186,6 +186,10 @@
TransformingReadOnlyCollection.cs
+
+
+ TransformingReadOnlyCollection.cs
+ WeakCollection.cs
diff --git a/Source/Collections/MultiDictionary.cs b/Source/Collections/MultiDictionary.cs
index 2b7d50f..b24a279 100644
--- a/Source/Collections/MultiDictionary.cs
+++ b/Source/Collections/MultiDictionary.cs
@@ -290,7 +290,7 @@ namespace Nuclex.Support.Collections {
}
}
- /// Number of elements contained in the Dictionary
+ /// Number of elements contained in the multi dictionary
public int Count {
get { return this.count; }
}
diff --git a/Source/Collections/Variegator.Test.cs b/Source/Collections/Variegator.Test.cs
new file mode 100644
index 0000000..7f0086e
--- /dev/null
+++ b/Source/Collections/Variegator.Test.cs
@@ -0,0 +1,84 @@
+#region CPL License
+/*
+Nuclex Framework
+Copyright (C) 2002-2013 Nuclex Development Labs
+
+This library is free software; you can redistribute it and/or
+modify it under the terms of the IBM Common Public License as
+published by the IBM Corporation; either version 1.0 of the
+License, or (at your option) any later version.
+
+This library is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+IBM Common Public License for more details.
+
+You should have received a copy of the IBM Common Public
+License along with this library
+*/
+#endregion
+
+using System;
+using System.Collections.Generic;
+
+#if UNITTEST
+
+using NUnit.Framework;
+
+namespace Nuclex.Support.Collections {
+
+ /// Unit Test for the Variegator multi dictionary
+ [TestFixture]
+ public class VariegatorTest {
+
+ ///
+ /// Tests whether the default constructor of the reverse comparer works
+ ///
+ [Test]
+ public void InstancesCanBeCreated() {
+ new Variegator();
+ }
+
+ ///
+ /// Verifies that querying for a missing value leads to an exception being thrown
+ ///
+ [Test]
+ public void QueryingMissingValueThrowsException() {
+ var variegator = new Variegator();
+ Assert.Throws(
+ () => {
+ variegator.Get(123);
+ }
+ );
+ }
+
+ ///
+ /// Verifies that the variegator resolves ambiguous matches according to its design
+ ///
+ [Test]
+ public void AmbiguityResolvesToLeastRecentValue() {
+ var variegator = new Variegator();
+ variegator.Add(1, "one");
+ variegator.Add(1, "eins");
+
+ string first = variegator.Get(1);
+ string second = variegator.Get(1);
+
+ // The variegator should have selected the first value by random and then
+ // returned the other value on the second query
+ Assert.AreNotEqual(first, second);
+
+ // Now the variegator should return the first value again because it is
+ // the least recently used value
+ Assert.AreEqual(first, variegator.Get(1));
+
+ // Repeating the query, the second should be returned again because now
+ // it has become the least recently used value
+ Assert.AreEqual(second, variegator.Get(1));
+ }
+
+ }
+
+} // namespace Nuclex.Support.Collections
+
+#endif // UNITTEST
diff --git a/Source/Collections/Variegator.cs b/Source/Collections/Variegator.cs
new file mode 100644
index 0000000..4e03563
--- /dev/null
+++ b/Source/Collections/Variegator.cs
@@ -0,0 +1,283 @@
+#region CPL License
+/*
+Nuclex Native Framework
+Copyright (C) 2002-2013 Nuclex Development Labs
+
+This library is free software; you can redistribute it and/or
+modify it under the terms of the IBM Common Public License as
+published by the IBM Corporation; either version 1.0 of the
+License, or (at your option) any later version.
+
+This library is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+IBM Common Public License for more details.
+
+You should have received a copy of the IBM Common Public
+License along with this library
+*/
+#endregion // CPL License
+
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Nuclex.Support.Collections {
+
+ // ------------------------------------------------------------------------------------------- //
+
+ /// Randomly selects between different options, trying to avoid repetition
+ /// Type of keys through which values can be looked up
+ /// Type of values provided by the variegator
+ ///
+ ///
+ /// This class is useful wherever randomness is involved in a game: picking random
+ /// actions for an NPC to execute, selecting different songs to play, displaying
+ /// different dialogue and more.
+ ///
+ ///
+ /// In principle, it works like a multimap, associating keys with a number of values
+ /// and allowing you to look up a values by their keys. Unlike a multimap, it will
+ /// avoid handing out a previously provided value again.
+ ///
+ ///
+ /// A typical usage would be to set up a mapping between situations and dialogue lines.
+ /// Upon calling with the situation 'detected-player-stealing',
+ /// the variegator would return a random (but not recently used) value which in this case
+ /// might contain a commentary an NPC might make upon encountering that situation.
+ /// Other NPCs requesting dialogue lines for the same situation would receive different
+ /// random commentary for as long as long as available data allows.
+ ///
+ ///
+ public class Variegator {
+
+ /// Initializes a new variegator
+ ///
+ /// How far into the past the variegator will look to avoid repetition
+ ///
+ public Variegator(int historyLength = 64) {
+ this.historyLength = historyLength;
+ this.history = new TValue[historyLength];
+ this.values = new MultiDictionary();
+
+ this.randomNumberGenerator = new Random();
+ }
+
+ /// Removes all entries from the variegator
+ ///
+ /// This is mainly useful if you are storing smart pointers to values of substantial
+ /// size (eg. audio clips instead of just resource proxies or paths) and need to
+ /// reclaim memory.
+ ///
+ public void Clear() {
+ freeHistory();
+ this.historyFull = false;
+ this.historyTailIndex = 0;
+ }
+
+ /// Checks whether the variegator is empty
+ /// True if there are no entries in the variegator
+ public bool IsEmpty {
+ get { return (Count == 0); }
+ }
+
+ /// Returns the number of values in the variegator
+ /// The number of values stored in the variegator
+ ///
+ /// If the same value is added with different keys (a situation that doesn't make
+ /// sense because such reuse should be covered by specifying multiple keys in
+ /// a query), it will be counted multiple times.
+ ///
+ public int Count {
+ get { return ((System.Collections.ICollection)this.values).Count; }
+ }
+
+ ///
+ /// Insert a new value that can be returned when requesting the specified key
+ ///
+ /// Key of the value that will be inserted
+ /// Value that will be inserted under the provided key
+ public void Add(TKey key, TValue value) {
+ this.values.Add(key, value);
+ }
+
+ /// Retrieves a random value associated with the specified key
+ /// For for which a value will be looked up
+ /// A random value associated with the specified key
+ public TValue Get(TKey key) {
+ ISet candidates = new HashSet();
+ {
+ ICollection valueRange = this.values[key];
+
+ // If possible access the values by index because it's faster and produces less
+ // garbage, otherwise fall back to using an enumerator
+ var indexableValueRange = valueRange as IList;
+ if(indexableValueRange == null) {
+ foreach(TValue value in valueRange) {
+ candidates.Add(value);
+ }
+ } else {
+ for(int valueIndex = 0; valueIndex < indexableValueRange.Count; ++valueIndex) {
+ candidates.Add(indexableValueRange[valueIndex]);
+ }
+ }
+ }
+
+ TValue result = destructivePickCandidateValue(candidates);
+ addRecentlyUsedValue(result);
+ return result;
+ }
+
+ /// Retrieves a random value associated with one of the specified keys
+ /// Keys that will be considered
+ ///
+ /// In many cases, you have generic situations (such as 'detected-player-stealing',
+ /// 'observed-hostile-action') and specified situations (such as
+ /// 'detected-player-stealing-from-beggar', 'observed-hostile-action-on-cop')
+ /// where a values from both pools should be considered. This method allows you
+ /// to specify any number of keys, creating a greater set of values the variegator
+ /// can pick between.
+ ///
+ public TValue Get(params TKey[] keys) {
+ ISet candidates = new HashSet();
+
+ for(int index = 0; index < keys.Length; ++index) {
+ ICollection valueRange = this.values[keys[index]];
+
+ // If possible access the values by index because it's faster and produces less
+ // garbage, otherwise fall back to using an enumerator
+ var indexableValueRange = valueRange as IList;
+ if(indexableValueRange == null) {
+ foreach(TValue value in valueRange) {
+ candidates.Add(value);
+ }
+ } else {
+ for(int valueIndex = 0; valueIndex < indexableValueRange.Count; ++valueIndex) {
+ candidates.Add(indexableValueRange[valueIndex]);
+ }
+ }
+ }
+
+ TValue result = destructivePickCandidateValue(candidates);
+ addRecentlyUsedValue(result);
+ return result;
+ }
+
+ /// Picks amongst the values in a set
+ ///
+ /// Set containing the candidats values to consider. Will be destroyed.
+ ///
+ /// The least recently used candidate value or a random one
+ private TValue destructivePickCandidateValue(ISet candidates) {
+ removeRecentlyUsedValues(candidates);
+
+ switch(candidates.Count) {
+ case 0: {
+ throw new InvalidOperationException("No values mapped to this key");
+ }
+ case 1: {
+ using(IEnumerator enumerator = candidates.GetEnumerator()) {
+ enumerator.MoveNext(); // We can be sure this one returns true
+ return enumerator.Current;
+ }
+ }
+ default: {
+ int index = this.randomNumberGenerator.Next(candidates.Count);
+ using(IEnumerator enumerator = candidates.GetEnumerator()) {
+ do {
+ --index;
+ enumerator.MoveNext(); // We can be sure this one returns true
+ } while(index >= 0);
+
+ return enumerator.Current;
+ }
+
+ throw new InvalidOperationException(
+ "ISet.Count was off or random number generator malfunctioned"
+ );
+ }
+ }
+ }
+
+ /// Adds a recently used value to the history
+ /// Value that will be added to the history
+ private void addRecentlyUsedValue(TValue value) {
+ if(this.historyTailIndex == this.historyLength) {
+ this.historyFull = true;
+ this.history[0] = value;
+ this.historyTailIndex = 1;
+ } else {
+ this.history[this.historyTailIndex] = value;
+ ++this.historyTailIndex;
+ }
+ }
+
+ /// Removes all values that are in the recent use list from a set
+ /// Set from which recently used values are removed
+ ///
+ /// Stops removing values when there's only 1 value left in the set
+ ///
+ private void removeRecentlyUsedValues(ISet candidates) {
+ if(candidates.Count <= 1) {
+ return;
+ }
+
+ if(this.historyFull) { // History buffer has wrapped around
+ int index = this.historyTailIndex;
+ while(index > 0) {
+ --index;
+ if(candidates.Remove(this.history[index])) {
+ if(candidates.Count <= 1) {
+ return;
+ }
+ }
+ }
+ index = this.historyLength;
+ while(index > this.historyTailIndex) {
+ --index;
+ if(candidates.Remove(this.history[index])) {
+ if(candidates.Count <= 1) {
+ return;
+ }
+ }
+ }
+ } else { // History buffer was not full yet
+ int index = this.historyTailIndex;
+ while(index > 0) {
+ --index;
+ if(candidates.Remove(this.history[index])) {
+ if(candidates.Count <= 1) {
+ return;
+ }
+ }
+ }
+ }
+ }
+
+ /// Frees all memory used by the individual history entries
+ ///
+ /// The history array itself is kept alive and the tail index + full flag will
+ /// not be reset.
+ ///
+ private void freeHistory() {
+ Array.Clear(this.history, 0, this.historyLength);
+ }
+
+ /// Stores the entries the variegator can select from by their keys
+ private IMultiDictionary values;
+
+ /// Random number generator that will be used to pick random values
+ private Random randomNumberGenerator;
+ /// Number of entries in the recently used list
+ private int historyLength;
+
+ /// Array containing the most recently provided values
+ private TValue[] history;
+ /// Index of the tail in the recently used value array
+ private int historyTailIndex;
+ /// Whether the recently used value history is at capacity
+ private bool historyFull;
+
+ }
+
+} // namespace Nuclex { namespace Support { namespace Collections
diff --git a/Source/IO/PartialStream.cs b/Source/IO/PartialStream.cs
index 7083f47..50724cf 100644
--- a/Source/IO/PartialStream.cs
+++ b/Source/IO/PartialStream.cs
@@ -43,18 +43,18 @@ namespace Nuclex.Support.IO {
throw new ArgumentException("Start index must not be less than 0", "start");
}
- if(!stream.CanSeek) {
- if(start != 0) {
- throw new ArgumentException(
- "The only valid start for unseekable streams is 0", "start"
- );
- }
- } else {
+ if(stream.CanSeek) {
if(start + length > stream.Length) {
throw new ArgumentException(
"Partial stream exceeds end of full stream", "length"
);
}
+ } else {
+ if(start != 0) {
+ throw new ArgumentException(
+ "The only valid start for unseekable streams is 0", "start"
+ );
+ }
}
this.stream = stream;
@@ -258,7 +258,6 @@ namespace Nuclex.Support.IO {
/// Length of the partial stream
private long length;
-
}
} // namespace Nuclex.Support.IO