/// <summary> /// Pick a random sample from the set, optionally with the specified gender. /// If a sample is rare it will be picked less often (using the rarity value). /// </summary> public Sample Pick(Sample.Genders gender = Sample.Genders.NEUTRAL) { if (!HasGender && gender != Sample.Genders.NEUTRAL) { throw new Exception("Cannot pick a gendered sample from a set of un-gendered samples"); } // If gender is not specified choose male or female at random if (HasGender && gender == Sample.Genders.NEUTRAL) { if (random.Next(2) == 0) { gender = Sample.Genders.MALE; } else { gender = Sample.Genders.FEMALE; } } // Copy the sample list so we can pick at random and // remove so we don't pick the same sample again. List <Sample> samples; if (!HasGender || gender == Sample.Genders.MALE) { samples = new List <Sample>(SampleList); } else { samples = new List <Sample>(FemaleSampleList); } // Pick a sample and, if it's rare, apply rarity percent and // if it isn't chosen, remove it from the list and pick again. // There is always at least one non-rare sample so the list // will never be exhausted. while (true) { int pickPos = random.Next(samples.Count); Sample sample = samples[pickPos]; if (sample.Rarity > 0) { // Roll the dice to see if we pick this rare sample if (random.Next(100) >= sample.Rarity) { // Remove from list and pick again samples.RemoveAt(pickPos); continue; } } // Got our sample return(sample); } }
/// <summary> /// Ref function /// </summary> public string FuncRef(string called, string[] args, Cache cache, string parent, IResolver resolver, out Sample.Genders gender) { string help = "Use func.ref(--help) for help."; if (args.Length == 1 && args[0].Equals("--help", StringComparison.CurrentCultureIgnoreCase)) { // Show usage string usage = "Usage: func.ref(arg) where arg is another attribute to take the value from."; throw new Exception(usage); } if (args.Length != 1) { throw new Exception($"{called} has bad number of arguments. {help}"); } if (args[0].Length == 0) { throw new Exception($"{called} has empty argument. {help}"); } ResolvedValue refValue; try { refValue = GetRefValue(args[0], cache, parent, resolver); } catch (Exception e) { throw new Exception($"{called} {e.Message}. {help}"); } gender = refValue.Gender; return(refValue.Value); }
/// <summary> /// Evaluates a single function. Function name must be a known /// function or an exception will be thrown. /// /// The arg list is resolved (as a whole) before being split into /// separate arguments so it may contain embedded func.xxx() calls. /// </summary> private string eval(string funcName, string argList, Cache cache, string parentName, out Sample.Genders gender) { string called = $"func.{funcName}({argList})"; // Validate function name Funcs.FuncNames func; try { func = (Funcs.FuncNames)Enum.Parse(typeof(Funcs.FuncNames), funcName.Trim(), true); } catch (Exception) { throw new Exception($"Unknown function 'func.{funcName}(...)'. Must be one of: {String.Join(", ", Enum.GetNames(typeof(Funcs.FuncNames)))}"); } // Resolve arg list (in case funcs are being passed as args to other funcs) but don't add // to cache as we want them to evaluate differently each time (if they're random). string nocacheParent; int sep = parentName.IndexOf('.'); if (sep == -1) { nocacheParent = parentName + ".NOCACHE"; } else { nocacheParent = parentName.Substring(0, sep) + ".NOCACHE"; } var resolvingArgs = new ResolvedValue(nocacheParent, Entity.Types.STR, argList); ResolveValue(resolvingArgs, cache); gender = resolvingArgs.Gender; // Replace escaped brackets and split up arguments var args = SplitArgs(resolvingArgs.Value.Replace("\\(", "(").Replace("\\)", ")").Trim()); // Call the relevant function string result; Sample.Genders newGender; switch (func) { case Funcs.FuncNames.DATE: return(this.funcs.FuncDate(called, args)); case Funcs.FuncNames.GEN: return(this.funcs.FuncGen(called, args)); case Funcs.FuncNames.IF: return(this.funcs.FuncIf(called, args, cache, parentName, this, this.formatter)); case Funcs.FuncNames.MATH: return(this.funcs.FuncMath(called, args, cache, parentName, this)); case Funcs.FuncNames.NUM: return(this.funcs.FuncNum(called, args, cache, parentName, this)); case Funcs.FuncNames.PICK: return(this.funcs.FuncPick(called, args)); case Funcs.FuncNames.RAND: return(this.funcs.FuncRand(called, args)); case Funcs.FuncNames.REF: result = this.funcs.FuncRef(called, args, cache, parentName, this, out newGender); if (newGender != Sample.Genders.NEUTRAL) { gender = newGender; } return(result); case Funcs.FuncNames.SAMPLE: result = this.funcs.FuncSample(called, args, cache, parentName, this, out newGender); if (newGender != Sample.Genders.NEUTRAL) { gender = newGender; } return(result); case Funcs.FuncNames.SPLIT: return(this.funcs.FuncSplit(called, args, cache, parentName, this)); case Funcs.FuncNames.STR: return(this.funcs.FuncStr(called, args)); case Funcs.FuncNames.TIME: return(this.funcs.FuncTime(called, args)); default: throw new Exception($"Missing function name: {func}"); } }
/// <summary> /// Takes a literal string and looks for any embedded /// func.xxx(...) calls. Each embedded function is resolved /// and the result is included in the returned string. /// /// Throws an exception if the value cannot be resolved /// </summary> private void ResolveValue(ResolvedValue resolving, Cache cache) { // See if it's already been resolved var resolved = cache.GetResolved(resolving.Name); if (resolved != null) { resolving.SetValue(resolved.Value, resolved.Gender); return; } if (resolving.UnresolvedValue == null) { throw new Exception("Unresolved value cannot be null"); } // If value is already concrete it's resolved string value = resolving.UnresolvedValue; if (!value.Contains("func.")) { resolving.SetValue(value); cache.SetResolved(resolving); return; } StringBuilder resolvedStr = new StringBuilder(); Sample.Genders gender = Sample.Genders.NEUTRAL; int idx = 0; while (idx < value.Length) { int funcPos = value.IndexOf("func.", idx, StringComparison.CurrentCultureIgnoreCase); if (funcPos == -1) { resolvedStr.Append(value.Substring(idx)); break; } resolvedStr.Append(value.Substring(idx, funcPos - idx)); // Find function name idx = funcPos + 5; int argStart = value.IndexOf('(', idx); if (argStart == -1) { throw new Exception($"Function 'func.{value.Substring(idx)}' has missing opening bracket"); } string funcName = value.Substring(idx, argStart - idx).Trim(); // Find matching close bracket argStart++; int argEnd = argStart; int openBrackets = 0; while (true) { if (argEnd == value.Length) { throw new Exception($"Function 'func.{value.Substring(idx)}' has missing closing bracket"); } // Ignore escaped brackets if (value[argEnd] == '\\' && argEnd < value.Length - 1 && (value[argEnd + 1] == '(' || value[argEnd + 1] == ')')) { argEnd += 2; continue; } if (value[argEnd] == '(') { openBrackets++; } else if (value[argEnd] == ')') { if (openBrackets == 0) { break; } else { openBrackets--; } } argEnd++; } string argList = value.Substring(argStart, argEnd - argStart); string parentName; int pos = resolving.Name == null? -1 : resolving.Name.LastIndexOf('.'); if (pos == -1) { parentName = resolving.Name; } else { parentName = resolving.Name.Substring(0, pos); } resolvedStr.Append(eval(funcName, argList, cache, parentName, out var newGender)); if (newGender != Sample.Genders.NEUTRAL) { gender = newGender; } idx = argEnd + 1; } resolving.SetValue(resolvedStr.ToString(), gender); cache.SetResolved(resolving); }
public Samples LoadSamples(string name, string[] lines) { int numCols = 1; int genderCol = 0; int rarityCol = 0; bool hasMaleNonRare = false; bool hasFemaleNonRare = false; int i = 0; // Skip blank lines and comments that don't contain a comma while (i < lines.Length && (lines[i].Trim().Length == 0 || (lines[i][0] == '#' && !lines[i].Contains(',')))) { i++; } // Is there a header? if (i < lines.Length && lines[i][0] == '#' && lines[i].Contains(',')) { var colNames = lines[i].Split(','); numCols = colNames.Length; if (numCols > 3) { throw new Exception($"Multi-column samples '{name}' header must have 2 or 3 columns"); } // Don't validate name of first column (can be anything) for (int col = 1; col < numCols; col++) { string colName = colNames[col].Trim(); Columns colType; try { colType = (Columns)Enum.Parse(typeof(Columns), colName, true); } catch (ArgumentException) { throw new Exception($"Multi-column samples '{name}' header has invalid column name '{colName}' - " + $"must be one of: {String.Join(", ", Enum.GetNames(typeof(Columns)))}"); } switch (colType) { case Columns.GENDER: genderCol = col; break; case Columns.RARITY: rarityCol = col; break; default: throw new Exception($"SamplesParser missing code for column type: {colType}"); } } i++; } var samples = new Samples(name, genderCol != 0); while (i < lines.Length) { string value = lines[i].Trim(); // Skip empty lines and comments if (value.Length == 0 || value[0] == '#') { i++; continue; } Sample.Genders gender = Sample.Genders.NEUTRAL; int rarity = 0; if (numCols > 1) { var columns = value.Split(','); if (columns.Length > numCols) { throw new Exception($"Multi-column samples file '{name}' line {i + 1} " + $"has too many columns (or embedded comma) - must match number of columns in header."); } string[] colData = new string[numCols]; for (int col = 0; col < numCols; col++) { if (col < columns.Length) { colData[col] = columns[col].Trim(); } else { colData[col] = ""; } } value = colData[0]; if (value.Length == 0) { throw new Exception($"Multi-column samples file '{name}' line {i + 1} " + $"value cannot be blank. Use <null> if you want a null value."); } if (genderCol != 0) { string genderStr = colData[genderCol].ToUpper(); if (genderStr.Length > 0) { if (genderStr.Equals("M")) { gender = Sample.Genders.MALE; } else if (genderStr.Equals("F")) { gender = Sample.Genders.FEMALE; } else { throw new Exception($"Multi-column samples file '{name}' line {i + 1} " + $"has gender '{genderStr}' but expected M or F"); } } } if (rarityCol != 0) { string rarityStr = colData[rarityCol]; if (rarityStr.Length > 0) { try { rarity = int.Parse(rarityStr); } catch (Exception) { throw new Exception($"Multi-column samples file '{name}' line {i + 1} " + $"has non-integer rarity '{rarityStr}'"); } if (rarity < 1 || rarity > 99) { throw new Exception($"Multi-column samples file '{name}' line {i + 1} " + $"has rarity {rarity} but expected either blank or value between 1 and 99"); } } } } if ((gender == Sample.Genders.MALE || gender == Sample.Genders.NEUTRAL) && rarity == 0) { hasMaleNonRare = true; } if ((gender == Sample.Genders.FEMALE || gender == Sample.Genders.NEUTRAL) && rarity == 0) { hasFemaleNonRare = true; } samples.AddSample(new Sample(value, gender, rarity)); i++; } // Samples must contain at least one sample. If gendered, must be // true for both male and female. If rarity, there must be at least // one non-rare sample. if (!hasMaleNonRare) { StringBuilder modifier = new StringBuilder(); if (genderCol != 0) { modifier.Append(" Male"); } if (rarityCol != 0) { modifier.Append(modifier.Length == 0? " " : ", "); modifier.Append("Non-rare (rarity left blank)"); } throw new Exception($"Samples file '{name}' " + $"must have at least one{modifier} sample"); } if (genderCol != 0 && !hasFemaleNonRare) { StringBuilder modifier = new StringBuilder(); if (genderCol != 0) { modifier.Append(" Female"); } if (rarityCol != 0) { modifier.Append(modifier.Length == 0 ? " " : ", "); modifier.Append("Non-rare (rarity left blank)"); } throw new Exception($"Multi-column samples file '{name}' " + $"must have at least one{modifier} sample"); } return(samples); }
public void SetValue(string value, Sample.Genders gender = Sample.Genders.NEUTRAL) { Value = value; Gender = gender; }
/// <summary> /// Sample function /// /// The sample function has a circular dependency on the resolver (so we can /// have samples based on the gender of other fields which may not be resolved /// yet) so we have to pass the resolver in as a parameter. /// </summary> public string FuncSample(string called, string[] args, Cache cache, string parent, IResolver resolver, out Sample.Genders gender) { string help = "Use func.sample(--help) for help."; if (args.Length == 1 && args[0].Equals("--help", StringComparison.CurrentCultureIgnoreCase)) { // Show usage string usage = "Usage: func.sample(arg1, [arg2]) where arg1 is the name of the samples file to use and arg2 is " + "either another attribute to take the gender from, or M or F to use a fixed gender."; throw new Exception(usage); } if (args.Length < 1 || args.Length > 2) { throw new Exception($"{called} has bad number of arguments. {help}"); } if (args[0].Length == 0) { throw new Exception($"{called} has empty argument. {help}"); } gender = Sample.Genders.NEUTRAL; if (args.Length == 2) { if (args[1].Equals("M", StringComparison.CurrentCultureIgnoreCase)) { gender = Sample.Genders.MALE; } else if (args[1].Equals("F", StringComparison.CurrentCultureIgnoreCase)) { gender = Sample.Genders.FEMALE; } else { // Inherit gender from referenced attribute. string refName; if (args[1].Length > 1 && args[1][0] == '~') { refName = parent + "." + args[1].Substring(1); } else { refName = parent + "." + args[1]; } Entity refAttrib; try { refAttrib = resolver.FindEntity(refName); } catch (Exception) { throw new Exception($"{called} references unknown attribute '{refName}'"); } var refValue = new ResolvedValue(refName, refAttrib.Type, refAttrib.Value); try { resolver.Resolve(refValue, cache); } catch (Exception e) { throw new Exception($"{called} references unresolvable attribute '{refName}': {e.Message}"); } gender = refValue.Gender; } } var samples = resolver.GetSamples(args[0]); var sample = samples.Pick(gender); gender = sample.Gender; return(sample.Value); }