Index: cpp/frams/genetics/f4/f4_conv.cpp
===================================================================
--- cpp/frams/genetics/f4/f4_conv.cpp	(revision 1305)
+++ cpp/frams/genetics/f4/f4_conv.cpp	(revision 1313)
@@ -1,4 +1,4 @@
 // This file is a part of Framsticks SDK.  http://www.framsticks.com/
-// Copyright (C) 1999-2023  Maciej Komosinski and Szymon Ulatowski.
+// Copyright (C) 1999-2024  Maciej Komosinski and Szymon Ulatowski.
 // See LICENSE.txt for details.
 
@@ -189,5 +189,5 @@
 	}
 
-	char tmpLine[100];
+	char tmpLine[256];
 	MultiRange range = C->genoRange;
 
@@ -200,6 +200,6 @@
 			// new part object for firstend
 			// coordinates are left to be computed by Model
-			sprintf(tmpLine, "fr=%g,ing=%g,as=%g",
-				/*1.0/C->P.mass,*/ C->P.friction, C->P.ingestion, C->P.assimilation
+			sprintf(tmpLine, "fr=%g,ing=%g,as=%g,vr=%g,vg=%g,vb=%g",
+				/*1.0/C->P.mass,*/ C->P.friction, C->P.ingestion, C->P.assimilation, C->P.cred, C->P.cgreen, C->P.cblue
 				//C->firstend.x, C->firstend.y, C->firstend.z
 			);
@@ -216,7 +216,7 @@
 		}
 		// new part object for lastend
-		sprintf(tmpLine, "fr=%g,ing=%g,as=%g",
+		sprintf(tmpLine, "fr=%g,ing=%g,as=%g,vr=%g,vg=%g,vb=%g",
 			//C->lastend.x, C->lastend.y, C->lastend.z
-			/*"vol=" 1.0/C->P.mass,*/ C->P.friction, C->P.ingestion, C->P.assimilation
+			/*"vol=" 1.0/C->P.mass,*/ C->P.friction, C->P.ingestion, C->P.assimilation, C->P.cred, C->P.cgreen, C->P.cblue
 		);
 		C->p2_refno = addFromString(PartType, tmpLine, &range);
@@ -241,4 +241,5 @@
 		C->joint_refno = addFromString(JointType, tmpLine, &range);
 		if (C->joint_refno < 0) return -13;
+		getJoint(C->joint_refno)->vcolor = (getPart(jj_p1_refno)->vcolor + getPart(jj_p2_refno)->vcolor) / 2; //joint gets the average color of both connected parts
 		this->checkpoint();
 	}
Index: cpp/frams/genetics/f4/f4_general.cpp
===================================================================
--- cpp/frams/genetics/f4/f4_general.cpp	(revision 1305)
+++ cpp/frams/genetics/f4/f4_general.cpp	(revision 1313)
@@ -1,4 +1,4 @@
 // This file is a part of Framsticks SDK.  http://www.framsticks.com/
-// Copyright (C) 1999-2023  Maciej Komosinski and Szymon Ulatowski.
+// Copyright (C) 1999-2024  Maciej Komosinski and Szymon Ulatowski.
 // See LICENSE.txt for details.
 
@@ -1414,5 +1414,5 @@
 			char *ptr = (char*)(genot + pos_inout);
 			string original = "";
-			while (GenoOperators::strchrn0(all_modifiers_no_comma, *ptr)) //only processes a section of chars known in all_modifiers_no_comma, other characters will exit the loop
+			while (GenoOperators::strchr_no0(all_modifiers_no_comma, *ptr)) //only processes a section of chars known in all_modifiers_no_comma, other characters will exit the loop
 			{
 				original += *ptr;
@@ -1422,9 +1422,9 @@
 			if (advanced > 0) //found modifiers
 			{
-				string simplified = GenoOperators::simplifiedModifiers(original);
+				string simplified = GenoOperators::simplifiedModifiers(original, F14_MODIFIERS_COLOR);
 				// add a node for each char in "simplified"
 				for (size_t i = 0; i < simplified.length(); i++)
 				{
-					int pos = GenoOperators::strchrn0(genot + pos_inout, simplified[i]) - genot; //unnecessarily finding the same char, if it occurrs multiple times in simplified
+					int pos = GenoOperators::strchr_no0(genot + pos_inout, simplified[i]) - genot; //unnecessarily finding the same char, if it occurrs multiple times in simplified
 					f4_Node *node = new f4_Node(simplified[i], par, pos); //location is approximate. In the simplification process we don't trace where the origin(s) of the simplified[i] gene were. We provide 'pos' as the first occurrence of simplified[i] (for example, all 'L' will have the same location assigned, but at least this is where 'L' occurred in the genotype, so in case of any modification of a node (repair, removal, whatever... even mapping of genes) the indicated gene will be one of the responsible ones)
 					par = node;
Index: cpp/frams/genetics/f4/f4_oper.cpp
===================================================================
--- cpp/frams/genetics/f4/f4_oper.cpp	(revision 1305)
+++ cpp/frams/genetics/f4/f4_oper.cpp	(revision 1313)
@@ -1,4 +1,4 @@
 // This file is a part of Framsticks SDK.  http://www.framsticks.com/
-// Copyright (C) 1999-2023  Maciej Komosinski and Szymon Ulatowski.
+// Copyright (C) 1999-2024  Maciej Komosinski and Szymon Ulatowski.
 // See LICENSE.txt for details.
 
@@ -17,5 +17,5 @@
 // TODO add mapping genotype character ranges for neural [connections]
 // TODO The f0 genotypes for /*4*/<<RX>X>X> and RX(X,X) are identical, but if you replace R with C or Q, there are small differences (they were present both before and after the change in C,Q effects in the f1 converter in 2023-06, see conv_f1_f0_cq_influence) - check why (modifiers affecting cells=sticks are applied differently or skip some initial sticks?) and perhaps unify with f1?
-// TODO F4_SIMPLIFY_MODIFIERS in f4_general.cpp: currently it works while parsing (which is a bit "cheating": we get a phenotype that is a processed version of the genotype, thus some changes in modifiers in the genotype have no effect on its phenotype). Another (likely better) option, instead of simplifying while parsing, would be during mutations (like it is done in f1): when mutations add/modify/remove a modifier node, they could "clean" the tree by simplifying modifiers on the same subpath just as GenoOperators::simplifiedModifiers() does. This way, simplifying would be only performed when we actually modify a part of a genotype, not each time we interpret it, and there would be no hidden mechanism: all visible genes would have an expected effect on the phenotype.
+// TODO F4_SIMPLIFY_MODIFIERS in f4_general.cpp: currently it works while parsing (which is a bit "cheating": we get a phenotype that is a processed version of the genotype, thus some changes in modifiers in the genotype have no effect on its phenotype). This is especially noticeable with color modifiers, where simplification only accepts MAX_NUMBER_SAME_TYPE_COLOR. Another (likely better) option, instead of simplifying while parsing, would be during mutations (like it is done in f1): when mutations add/modify/remove a modifier node, they could "clean" the tree by simplifying modifiers on the same subpath just as GenoOperators::simplifiedModifiers() does. This way, simplifying would be only performed when we actually modify a part of a genotype, not each time we interpret it, and there would be no hidden mechanism: all visible genes would have an expected effect on the phenotype. In other words, if a genotype happened to have more modifiers of the same type than allowed by GenoOperators::simplifiedModifiers(), they would all be effective (like in f1) and not silently ignored when interpreting that genotype. For example in /*4*/GGGgGX> due to simplifying modifiers (removing the oldest), only the last G remains effective when MAX_NUMBER_SAME_TYPE_COLOR=1, and it is impossible to adjust colors precisely even by manually editing the genotype - like it is in f1, even though the same simplifying function is used, just in a different moment.
 // TODO improve the way modifiers are handled in the f4->f1 approximate converter (used extremely rarely just for illustration)
 
@@ -32,5 +32,4 @@
 
 
-const char *Geno_f4::all_modifiers = F14_MODIFIERS ","; //comma in f4 is handled the same way (simple node, F4_ADD_SIMP) as modifiers. See also all_modifiers_no_comma in f4_general.cpp.
 
 // codes that can be changed (apart from being added/deleted)
@@ -56,5 +55,5 @@
 
 	{ "f4_mut_max_rep", 1, 0, "Maximum number for '#' repetitions", "d 2 20 6", FIELD(mut_max_rep), "Maximum allowed number of repetitions for the '#' repetition gene", },
-	{ "f4_mut_exmod", 1, 0, "Excluded modifiers", "s 0 30", FIELD(excluded_modifiers), "Modifiers that will not be added nor deleted during mutation\n(all: " F14_MODIFIERS ")", },
+	{ "f4_mut_modifiers", 1, 0, "Allowed modifiers", "s 0 100", FIELD(allowed_modifiers), "Modifier symbols that will be added or deleted during mutation\n(from the full set: " F14_MODIFIERS ").\n\nYou may use the extended syntax: after every allowed symbol, you may include its probability value in parentheses.\nWithout parentheses, all allowed symbols behave as if they had (1.0) appended.\nIf you include (0.0) after a symbol, this bans that symbol as if it was not present in this string.", },
 	{ 0, },
 };
@@ -84,5 +83,5 @@
 void Geno_f4::setDefaults()
 {
-	excluded_modifiers = F14_MODIFIERS_RARE F14_MODIFIERS_VISUAL;
+	allowed_modifiers = F14_MODIFIERS_BASIC F14_MODIFIERS_COLOR_SPORADIC;
 }
 
@@ -183,5 +182,20 @@
 			f4_Node *node_new = NULL; //stick or neuron or neural connection
 			if (rndDouble(1) < STICK_OR_NEURON)
+			{
 				node_new = new f4_Node('X', node_new_div, node_new_div->pos);
+				//now add one color modifier before this new X, just as in f1's Geno_f1::addMutationColoredX()
+				if (prob[F4_ADD] > 0 && probadd[F4_ADD_SIMP] > 0) //user wants modifier mutations, so we are allowed to prepend a random color modifier to "X"
+				{
+					char color_mod = GenoOperators::getRandomColorModifier(allowed_modifiers.c_str(), F14_MODIFIERS_COLOR); //may return 0 if no color modifiers available
+					if (color_mod != 0)
+					{
+						//adding a new color modifier node just as in the F4_ADD_SIMP case later below.
+						node_new->parent->removeChild(node_new);
+						f4_Node *n2 = new f4_Node(color_mod, node_new->parent, node_new->parent->pos);
+						n2->addChild(node_new);
+						node_new->parent = n2;
+					}
+				}
+			}
 			else
 			{
@@ -220,5 +234,5 @@
 				}
 			}
-			new f4_Node('>', node_new, node_new->pos); //adds to node_new
+			new f4_Node('>', node_new, node_new->pos); //adds '>' to node_new
 			node_mutated->parent = node_new_div;
 			// now, swap children with 50% chance
@@ -294,11 +308,12 @@
 		{
 			// add simple node
-			int modifier_index = GenoOperators::getRandomChar(all_modifiers, excluded_modifiers.c_str());
-			if (modifier_index < 0)
+			SString allowed_modifiers_and_comma = allowed_modifiers + ","; // traditionally, in f4, comma was treated here equally with modifiers
+			char modifier = GenoOperators::getRandomModifier(allowed_modifiers_and_comma.c_str());
+			if (modifier == 0)
 				return GENOPER_OPFAIL;
 			node_mutated->parent->removeChild(node_mutated);
 			// old source: choose a simple node from ADD_SIMPLE_CODES
 			//f4_Node *n2 = new f4_Node(ADD_SIMPLE_CODES[rndUint(strlen(ADD_SIMPLE_CODES))], node_mutated->parent, node_mutated->parent->pos);
-			f4_Node *n2 = new f4_Node(all_modifiers[modifier_index], node_mutated->parent, node_mutated->parent->pos);
+			f4_Node *n2 = new f4_Node(modifier, node_mutated->parent, node_mutated->parent->pos);
 			n2->addChild(node_mutated);
 			node_mutated->parent = n2;
@@ -722,5 +737,11 @@
 	else if (strchr(">", ch))                style = GENSTYLE_RGBS(0, 0, 100, GENSTYLE_NONE);
 	else if (strchr(STYL4CAT_DIGIT, ch))     style = GENSTYLE_CS(GENCOLOR_NUMBER, GENSTYLE_NONE);
-	else if (strchr(STYL4CAT_MODIFIC, ch))   style = GENSTYLE_RGBS(100, 100, 100, GENSTYLE_NONE);
+	else if (strchr(STYL4CAT_MODIFIC, ch))
+	{
+		if (strchr(F14_MODIFIERS_COLOR, ch)) //color modifier - less important so less visible on white
+			style = GENSTYLE_RGBS(200, 200, 200, GENSTYLE_NONE);
+		else //non-color modifier
+			style = GENSTYLE_RGBS(100, 100, 100, GENSTYLE_NONE);
+	}
 	else if (strchr(STYL4CAT_NEUMOD, ch))    style = GENSTYLE_RGBS(0, 150, 0, GENSTYLE_NONE);
 	if (isalpha(ch))
Index: cpp/frams/genetics/f4/f4_oper.h
===================================================================
--- cpp/frams/genetics/f4/f4_oper.h	(revision 1305)
+++ cpp/frams/genetics/f4/f4_oper.h	(revision 1313)
@@ -1,4 +1,4 @@
 // This file is a part of Framsticks SDK.  http://www.framsticks.com/
-// Copyright (C) 1999-2023  Maciej Komosinski and Szymon Ulatowski.
+// Copyright (C) 1999-2024  Maciej Komosinski and Szymon Ulatowski.
 // See LICENSE.txt for details.
 
@@ -59,6 +59,5 @@
 	paInt mut_max_rep;                ///maximum allowed number of repetitions for the '#' repetition gene
 
-	SString excluded_modifiers;       ///<Modifiers that are excluded in mutation process
-	static const char *all_modifiers;
+	SString allowed_modifiers;        //to be used in mutations
 
 private:
Index: cpp/frams/genetics/geneprops.h
===================================================================
--- cpp/frams/genetics/geneprops.h	(revision 1305)
+++ cpp/frams/genetics/geneprops.h	(revision 1313)
@@ -1,4 +1,4 @@
 // This file is a part of Framsticks SDK.  http://www.framsticks.com/
-// Copyright (C) 1999-2023  Maciej Komosinski and Szymon Ulatowski.
+// Copyright (C) 1999-2024  Maciej Komosinski and Szymon Ulatowski.
 // See LICENSE.txt for details.
 
@@ -13,7 +13,10 @@
 
 
-#define F14_MODIFIERS_VISUAL "DdGgBb"
+#define F14_MODIFIERS_BASIC "LlRrCcQqFfMm"
 #define F14_MODIFIERS_RARE "EeWwSsAaIi" //An expdef would need to handle many of these properly/specifically to ensure reasonable behavior, and hardly any expdef does. Modifying initial energy of a creature as a result of its genes (Ee) is in general not a good idea, but may make sense in some specific competitive biological simulations. Weight (Ww) works only in water, and in water sinking/going up should usually be caused by real "intentional" activity of a creature, not by its inherited weight. Stamina (Ss) is no longer needed as destructive collisions are not supported, and even if they were, some expdef would need to impose reasonable restrictions on the value of this parameter (e.g. similar to normalizeBiol4()) so there is some cost associated with it, and the specific consequences of destructions should be defined as needed. For assimilation (Aa), there is a dedicated parameter in CreaturesGroup. Ingestion (Ii) influences how fast energy is transferred to Parts of a creature, and optimizing this property may make sense in experiments where energy is present in the environment and can be ingested - if needed, this modifier should be enabled for mutation.
-#define F14_MODIFIERS "LlRrCcQqFfMm" F14_MODIFIERS_RARE F14_MODIFIERS_VISUAL
+#define F14_MODIFIERS_COLOR "DdGgBb"
+#define F14_MODIFIERS_COLOR_SPORADIC "D(0.01)d(0.01)G(0.01)g(0.01)B(0.01)b(0.01)" //add a small chance so that color mutations can rarely occur. Functionally they are neutral so we do not want to waste computational resources on creating and evaluating such duplicate individuals, but it is nice to have visually distinct "lineages" by allowing propagated color genes to be extremely rarely modified. Also, if these color modifiers have positive probability, all newly created sticks are assigned a random color immediately on creation.
+
+#define F14_MODIFIERS     F14_MODIFIERS_BASIC  F14_MODIFIERS_RARE  F14_MODIFIERS_COLOR
 
 
Index: cpp/frams/genetics/genooperators.cpp
===================================================================
--- cpp/frams/genetics/genooperators.cpp	(revision 1305)
+++ cpp/frams/genetics/genooperators.cpp	(revision 1313)
@@ -1,4 +1,4 @@
 // This file is a part of Framsticks SDK.  http://www.framsticks.com/
-// Copyright (C) 1999-2023  Maciej Komosinski and Szymon Ulatowski.
+// Copyright (C) 1999-2024  Maciej Komosinski and Szymon Ulatowski.
 // See LICENSE.txt for details.
 
@@ -56,4 +56,9 @@
 	for (sum = 0, i = 0; i < count; i++) { sum += probtab[i]; if (sel < sum) return i; }
 	return -1;
+}
+
+int GenoOperators::roulette(const vector<double> &probtab)
+{
+	return roulette(probtab.data(), (int)probtab.size());
 }
 
@@ -461,22 +466,53 @@
 }
 
-char *GenoOperators::strchrn0(const char *str, char ch)
+char *GenoOperators::strchr_no0(const char *str, char ch)
 {
 	return ch == 0 ? NULL : strchr((char *)str, ch);
 }
 
-int GenoOperators::getRandomChar(const char *choices, const char *excluded)
-{
-	int allowed_count = 0;
-	for (size_t i = 0; i < strlen(choices); i++) if (!strchrn0(excluded, choices[i])) allowed_count++;
-	if (allowed_count == 0) return -1; //no char is allowed
-	int rnd_index = rndUint(allowed_count) + 1;
-	allowed_count = 0;
-	for (size_t i = 0; i < strlen(choices); i++)
-	{
-		if (!strchrn0(excluded, choices[i])) allowed_count++;
-		if (allowed_count == rnd_index) return int(i);
-	}
-	return -1; //never happens
+double GenoOperators::probOfModifier(const char* mod_def)
+{
+	if (*(mod_def + 1) == '(') //the special syntax with the appended probability value in (...)
+		return std::atof(mod_def + 2); //0.0 when no valid number
+	return 1.0;
+}
+
+char GenoOperators::getRandomModifier(const char *choices)
+{
+	static const char* EXTRA_CHARS = "().0123456789";
+	// this function assumes that EXTRA_CHARS are only used for the special probabilities syntax in "choices", not as valid choice characters.
+	vector<char> allowed; //this could be determined only once for a given "choices", as long as the effect of "choices" is deterministic (i.e., "choices" does not include probabilities)
+	size_t choices_len = strlen(choices);
+	allowed.reserve(choices_len); //max size, avoid reallocations later
+	for (size_t i = 0; i < choices_len; i++)
+	{
+		if (strchr(EXTRA_CHARS, choices[i])) continue; //skip parentheses and numbers
+		double prob = probOfModifier(&choices[i]);
+		if (prob == 1.0 || rndDouble(1) < prob)
+			allowed.push_back(choices[i]);
+	}
+	if (allowed.size() == 0) return 0; //no char is allowed
+	return allowed[rndUint(allowed.size())];
+}
+
+char GenoOperators::getRandomColorModifier(const char *choices, const char *color_modifiers)
+{
+	vector<char> allowed_colors;
+	vector<double> allowed_probs;
+	size_t colors_len = strlen(color_modifiers);
+	allowed_colors.reserve(colors_len); //max size, avoid reallocations later
+	allowed_probs.reserve(colors_len); //max size, avoid reallocations later
+	for (size_t i = 0; i < colors_len; i++) //for all known color modifiers...
+	{
+		const char *pos = strchr(choices, color_modifiers[i]); //...search in "choices" - i.e., in currently set active modifiers. Note that "choices" may use an extended syntax with numbers and parentheses, such as qM(0.1)Dm(0.1)dG(0.2)C
+		if (pos) //found the color modifier in choices
+		{
+			allowed_colors.push_back(*pos);
+			allowed_probs.push_back(probOfModifier(pos));
+		}
+	}
+	// the above "parsing" part could be done only once "choices" changes, not every time we want to get a random color modifier...
+	int idx = roulette(allowed_probs);
+	return idx < 0 ? 0 : allowed_colors[idx];
 }
 
@@ -544,7 +580,8 @@
 }
 
-string GenoOperators::simplifiedModifiers(const string & original)
+string GenoOperators::simplifiedModifiers(const string & original, const char* colorgenes)
 {
 	const int MAX_NUMBER_SAME_TYPE = 5; // max. number of modifiers of each type (case-insensitive). The more characters, the closer we can get to min and max values of a given property at the expense of the length of evolved genotypes. 5 is "close enough", but how close we get to the extreme also depends on the initial value of a given property, which is not always exactly in the middle of min and max. rR is treated separately in simplification because their influence follows different (i.e., simple additive) logic - so the simplifiedModifiersFixedOrder() logic with cancelling out antagonistic modifiers would be appropriate for rR.
+	const int MAX_NUMBER_SAME_TYPE_COLOR = 1; //color does not affect fitness and is used purely for aesthetics, so allow at most 1 char for each r,g,b channel - we get very low resolution of colors (only 3*3*3 combinations), but we spare the genotype length and limit bloat
 	int counter[256] = {}; //initialize with zeros; 256 is unnecessarily too big and redundant, but enables very fast access (indexed directly by the ascii code)
 	string simplified = "";
@@ -556,5 +593,6 @@
 		unsigned char lower = std::tolower(c);
 		counter[lower]++;
-		if (counter[lower] <= MAX_NUMBER_SAME_TYPE) //get rid of modifiers that are too numerous, but get rid of the first ones in the string (="oldest", the last ones looking from the end), because their influence on the parameter value is the smallest
+		int MAX_NUMBER = strchr(colorgenes, c) != NULL ? MAX_NUMBER_SAME_TYPE_COLOR : MAX_NUMBER_SAME_TYPE;
+		if (counter[lower] <= MAX_NUMBER) //get rid of modifiers that are too numerous - get rid of the first ones in the string (="oldest", the last ones looking from the end), because their influence on the parameter value is the smallest
 			simplified += c;
 	}
Index: cpp/frams/genetics/genooperators.h
===================================================================
--- cpp/frams/genetics/genooperators.h	(revision 1305)
+++ cpp/frams/genetics/genooperators.h	(revision 1313)
@@ -1,4 +1,4 @@
 // This file is a part of Framsticks SDK.  http://www.framsticks.com/
-// Copyright (C) 1999-2023  Maciej Komosinski and Szymon Ulatowski.
+// Copyright (C) 1999-2024  Maciej Komosinski and Szymon Ulatowski.
 // See LICENSE.txt for details.
 
@@ -144,5 +144,5 @@
 	virtual int mutate(char *&geno, float& chg, int &method) { method = -1; chg = -1; return GENOPER_NOOPER; }
 
-	/**Crosses over two genotypes. It is sufficient to return only one child (in \a g1) and set \a chg1 only, then \a g2 must equal "".
+	/**Crosses over two genotypes. It is sufficient to return only one child (in \a g1) and set \a chg1 only, then \a g2 must be "".
 
 	Avoid unnecessary calls in your code. Every genotype argument passed to this
@@ -183,5 +183,6 @@
 	static const int NEUROCLASS_PROP_OFFSET = 100; //a NeuroClass property is identified by some functions below as a single-value integer index, yet a property is either "standard" or "extra" (two separate lists), hence this offset to tell one case from the other.
 
-	static int roulette(const double *probtab, const int count); 	///<returns random index according to probabilities in the \a probtab table or -1 if all probs are zero. \a count is the number of elements in \a probtab.
+	static int roulette(const double *probtab, const int count); ///<returns a random index according to probabilities in the \a probtab table or -1 if all probs are zero. \a count is the number of elements in \a probtab.
+	static int roulette(const vector<double> &probtab); ///<returns a random index according to probabilities in the \a probtab table or -1 if all probs are zero.
 	static bool getMinMaxDef(ParamInterface *p, int propindex, double &mn, double &mx, double &def); ///<perhaps a more useful (higher-level) way to obtain min/max/def info for integer and double properties. Returns true if min/max/def was really available (otherwise it is just invented).
 	static bool mutateRandomNeuroClassProperty(Neuro* n); ///<high-level neuron mutation function, will select and mutate a random property of Neuron's NeuroClass. Returns true if successful and some property was actually mutated. Could return false when the NeuroClass of the Neuron have no properties, or when a randomly selected property was not suitable for mutation (for example a string or another non-number type).
@@ -215,11 +216,13 @@
 	static void skipWS(char *&s); ///<advances pointer \a s skipping whitespaces.
 	static bool areAlike(char*, char*); ///<compares two text strings skipping whitespaces. Returns 1 when equal, 0 when different.
-	static char* strchrn0(const char *str, char ch); ///<like strchr, but does not find ascii=0 char in \a str.
-
-	static int getRandomChar(const char *choices, const char *excluded); ///<returns index of a random character from \a choices excluding \a excluded, or -1 when everything is excluded or \a choices is empty.
+	static char* strchr_no0(const char *str, char ch); ///<like strchr, but does not find ascii=0 char in \a str.
+
+	static double probOfModifier(const char* mod_def); //returns a probability of a modifier: either 1.0 (default) or parsed value if the probability is given in the appended parentheses (...). For example, "G(0.3)" will return 0.3, and "G" will return 1.0.
+	static char getRandomModifier(const char *choices); ///<returns a random character from \a choices (note that the special syntax with probabilities in parentheses is supported), or 0 when \a choices is empty or probabilities were insufficient for a random chance to choose some character.
+	static char getRandomColorModifier(const char *choices, const char *color_modifiers); //finds all color_modifiers in choices and returns a color modifier drawn randomly proportionally to the optional probabilities defined in choices. Returns 0 when \a choices does not have any color modifier with a positive probability.
 	static string simplifiedModifiers_rR(const string& str); ///<finds all 'r' and 'R' in \a str and returns the shortest sequence of 'r' and 'R that is equivalent to all these found in \a str.
 	static string simplifiedModifiersFixedOrder(const char *str_of_char_pairs, vector<int> &char_counts); ///<returns a sequence of chars from \a str_of_char_pairs based on how many times each char occurred in \a char_counts. Assume that an even-index char and the following odd-index char have the opposite influence, so they cancel out. We don't use this function, because a fixed order imposed by this function means that the number of different parameter values produced by a sequence of modifiers is lowered (N same-letter upper- and lower-case chars yield only 2*N different values). Due to how modifiers work, the effect of aaA, aAa, Aaa etc. is different (N same-letter upper- and lower-case chars yield 2^N different values), so simplifying modifiers should not impose any order, should not interfere with their original order, and should not cancel out antagonistic modifiers - see \a simplifiedModifiers() and geneprops_test.cpp.
 	//@}
-	static string simplifiedModifiers(const string &original); ///<from the \a original sequence removes modifiers that are too numerous (exceeding a defined threshold number), starting the removal from the least-significant, leftmost (="oldest" when interpreting the sequence from left to right) ones. Contrary to \a simplifiedModifiersFixedOrder(), this kind of simplification preserves 2^N different sequences for each upper/lower-case modifier and thus 2^N different values of a given property (see geneprops.cpp), but the values resulting from these sequences constitute a landscape not as easy for optimization as in the case of 2*N, where the effect of each mutation could be independent and additive (no epistasis). So for a given sequence length, the 2^N case allows for a higher resolution at the cost of a more rugged fitness landscape than the 2*N case.
+	static string simplifiedModifiers(const string &original, const char* colorgenes); ///<from the \a original sequence removes modifiers that are too numerous (exceeding a defined threshold number), starting the removal from the least-significant, leftmost (="oldest" when interpreting the sequence from left to right) ones. Contrary to \a simplifiedModifiersFixedOrder(), this kind of simplification preserves 2^N different sequences for each upper/lower-case modifier and thus 2^N different values of a given property (see geneprops.cpp), but the values resulting from these sequences constitute a landscape not as easy for optimization as in the case of 2*N, where the effect of each mutation could be independent and additive (no epistasis). So for a given sequence length, the 2^N case allows for a higher resolution at the cost of a more rugged fitness landscape than the 2*N case.
 };
 
