worldbox-observer-mod/IdleSpectator/UnitDossier.cs

1004 lines
28 KiB
C#

using System;
using System.Collections.Generic;
using System.Text;
namespace IdleSpectator;
/// <summary>
/// Snapshot of why a living unit is interesting (caption + chronicle source).
/// </summary>
public sealed class UnitDossier
{
public long UnitId;
public string Name = "";
public string SpeciesId = "";
public string Headline = "";
public string ReasonLine = "";
public string DetailLine = "";
public string CaptionText = "";
public string JobLabel = "";
public float Score;
public int Kills;
public int Age;
public int Level;
public int Renown;
public bool IsFavorite;
public bool IsKing;
public bool IsCityLeader;
public bool IsFighting;
public bool IsSpectacle;
public bool IsLoneSpecies;
public bool IsMale;
public bool IsFemale;
public string TaskText = "";
public string KingdomName = "";
public string CityName = "";
public readonly List<string> ScoreReasons = new List<string>();
public readonly List<TraitChip> TopTraits = new List<TraitChip>();
public sealed class TraitChip
{
public string Id = "";
public string Name = "";
public ActorTrait Trait;
}
/// <summary>Harness: force JobLabel on the next dossier build (no live citizen_job).</summary>
public static string HarnessJobLabelOverride = "";
/// <summary>Harness: optional watch-reason fragment used with <see cref="HarnessJobLabelOverride"/>.</summary>
public static string HarnessReasonOverride = "";
public static void ClearHarnessOverrides()
{
HarnessJobLabelOverride = "";
HarnessReasonOverride = "";
}
/// <summary>
/// Minimal dossier for archive subjects with no living unit (harness orphans / legacy deaths).
/// </summary>
public static UnitDossier FromFallenArchive(
long unitId,
string name,
string speciesId,
DeathManner manner = DeathManner.None)
{
UnitDossier d = new UnitDossier();
d.UnitId = unitId;
d.Name = string.IsNullOrEmpty(name) ? "Nameless" : name;
d.SpeciesId = string.IsNullOrEmpty(speciesId) ? "creature" : speciesId;
d.Headline = string.IsNullOrEmpty(d.SpeciesId)
? d.Name
: $"{d.Name} ({d.SpeciesId})";
string mannerLabel = Chronicle.DeathMannerLabel(manner);
d.ReasonLine = string.IsNullOrEmpty(mannerLabel) || manner == DeathManner.None
? "Fallen"
: char.ToUpperInvariant(mannerLabel[0]) + mannerLabel.Substring(1);
d.DetailLine = "";
d.CaptionText = JoinCaption(d.Headline, d.ReasonLine, d.DetailLine);
return d;
}
public static UnitDossier FromActor(Actor actor, string watchLabel = null)
{
UnitDossier d = new UnitDossier();
if (actor == null || !actor.isAlive())
{
d.Headline = "Nobody";
d.ReasonLine = "";
d.DetailLine = "no living unit";
d.CaptionText = d.Headline;
return d;
}
Dictionary<string, int> speciesCounts = WorldActivityScanner.CountSpeciesPopulations();
d.UnitId = actor.getID();
d.Name = SafeName(actor);
d.SpeciesId = actor.asset != null ? actor.asset.id : "creature";
d.Kills = actor.data != null ? actor.data.kills : 0;
d.Age = SafeAge(actor);
d.Level = SafeLevel(actor);
d.Renown = actor.renown;
d.IsFavorite = actor.isFavorite();
d.IsKing = actor.isKing();
d.IsCityLeader = actor.isCityLeader();
d.IsFighting = actor.has_attack_target;
d.IsSpectacle = WorldActivityScanner.IsSpectaclePublic(actor);
d.IsLoneSpecies = WorldActivityScanner.IsSingletonSpeciesPublic(actor, speciesCounts);
d.KingdomName = actor.kingdom != null ? actor.kingdom.name : "";
d.CityName = actor.city != null ? actor.city.name : "";
d.Score = WorldActivityScanner.ScoreActorPublic(actor, speciesCounts);
FillSex(d, actor);
d.TaskText = SafeTask(actor);
FillTopTraits(d, actor);
CollectReasons(d, actor, speciesCounts);
d.JobLabel = ResolveJobLabel(actor);
// Species icon is shown separately; keep id in the title for scanability.
d.Headline = string.IsNullOrEmpty(d.SpeciesId)
? d.Name
: $"{d.Name} ({d.SpeciesId})";
d.ReasonLine = BuildStoryBeat(d, watchLabel, actor);
d.DetailLine = BuildDetailLine(d);
d.CaptionText = JoinCaption(d.Headline, d.ReasonLine, d.DetailLine);
return d;
}
private static string ResolveJobLabel(Actor actor)
{
if (!string.IsNullOrEmpty(HarnessJobLabelOverride))
{
return HarnessJobLabelOverride.Trim();
}
string jobId = ActivityInterestTable.SafeJobId(actor);
return ActivityProse.HumanizeJobPublic(jobId);
}
public bool ContainsIgnoreCase(string needle)
{
if (string.IsNullOrEmpty(needle))
{
return false;
}
if ((CaptionText ?? "").IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0
|| (Headline ?? "").IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0
|| (ReasonLine ?? "").IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0
|| (DetailLine ?? "").IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0
|| (Name ?? "").IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0
|| (SpeciesId ?? "").IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0
|| (TaskText ?? "").IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0)
{
return true;
}
// Harness probes "age" after we moved age onto an icon chip.
if (needle.Equals("age", System.StringComparison.OrdinalIgnoreCase) && Age > 0)
{
return true;
}
for (int i = 0; i < TopTraits.Count; i++)
{
TraitChip chip = TopTraits[i];
if (chip == null)
{
continue;
}
if ((!string.IsNullOrEmpty(chip.Name)
&& chip.Name.IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0)
|| (!string.IsNullOrEmpty(chip.Id)
&& chip.Id.IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0))
{
return true;
}
}
return false;
}
private static string JoinCaption(string headline, string reason, string detail)
{
StringBuilder sb = new StringBuilder();
if (!string.IsNullOrEmpty(headline))
{
sb.Append(headline);
}
if (!string.IsNullOrEmpty(reason))
{
if (sb.Length > 0)
{
sb.Append('\n');
}
sb.Append(reason);
}
if (!string.IsNullOrEmpty(detail))
{
if (sb.Length > 0)
{
sb.Append('\n');
}
sb.Append(detail);
}
return sb.ToString();
}
/// <summary>
/// Story beat for the dossier reason row: what is happening in this shot.
/// No tier badges, no job append, no nametag repeats.
/// Fill / ambient scenes with no sharper beat leave the reason empty (row hidden).
/// Follow-up (not blocking): expand event/character tags and optional beatPhrases
/// in scoring-model.json once playtests show thin beats.
/// </summary>
private static string BuildStoryBeat(UnitDossier d, string watchLabel, Actor actor)
{
if (!string.IsNullOrEmpty(HarnessReasonOverride))
{
return HarnessReasonOverride.Trim();
}
InterestCandidate scene = MatchingScene(actor, d);
string combat = CombatBeat(d, scene);
if (!string.IsNullOrEmpty(combat))
{
return combat;
}
string griefOrLife = HappinessBeat(scene, d);
if (!string.IsNullOrEmpty(griefOrLife))
{
return griefOrLife;
}
// Ambient filler: no story beat - hide the reason row.
if (scene != null && InterestScoring.IsFillScore(scene.TotalScore)
&& !InterestScoring.IsCombatAction(scene))
{
return "";
}
string fromScene = SceneLabelBeat(scene, d);
if (!string.IsNullOrEmpty(fromScene))
{
return fromScene;
}
string fromWatch = EventLabelBeat(watchLabel, d);
if (!string.IsNullOrEmpty(fromWatch))
{
return fromWatch;
}
if (d.IsSpectacle)
{
return "Spectacle";
}
string activity = ActivityBeat(actor);
if (!string.IsNullOrEmpty(activity))
{
return activity;
}
if (d.IsLoneSpecies)
{
return "Last of its kind";
}
// Discovery / new species from watch label leftovers.
string discovery = CleanBeat(ShortenWatchLabel(watchLabel, d));
if (!string.IsNullOrEmpty(discovery)
&& discovery.IndexOf("New species", System.StringComparison.OrdinalIgnoreCase) >= 0)
{
return "New species";
}
return "";
}
private static InterestCandidate MatchingScene(Actor actor, UnitDossier d)
{
InterestCandidate scene = InterestDirector.CurrentCandidate;
if (scene == null)
{
return null;
}
if (scene.FollowUnit == actor)
{
return scene;
}
if (actor != null && scene.SubjectId == d.UnitId)
{
return scene;
}
if (scene.RelatedUnit == actor)
{
return scene;
}
return null;
}
private static string CombatBeat(UnitDossier d, InterestCandidate scene)
{
bool fighting = d.IsFighting
|| (scene != null && InterestScoring.IsCombatAction(scene));
if (!fighting)
{
return "";
}
int fighters = scene != null ? scene.ParticipantCount : 0;
int notables = scene != null ? scene.NotableParticipantCount : 0;
if (fighters <= 0 && d.IsFighting)
{
fighters = 1;
notables = (d.IsKing || d.IsFavorite || d.IsCityLeader) ? 1 : 0;
}
if (fighters <= 2 && notables >= 2)
{
return "King duel";
}
if (fighters >= 4)
{
return "Clash · " + fighters + " fighting";
}
if (fighters >= 2)
{
return "Fighting · " + fighters;
}
return "Fighting";
}
private static string HappinessBeat(InterestCandidate scene, UnitDossier d)
{
if (scene == null)
{
return "";
}
string label = scene.Label ?? "";
if (label.StartsWith("Slain here", System.StringComparison.OrdinalIgnoreCase))
{
return CleanBeat(label);
}
if (string.IsNullOrEmpty(scene.HappinessEffectId))
{
return "";
}
string id = scene.HappinessEffectId;
if (id.StartsWith("death_", System.StringComparison.OrdinalIgnoreCase))
{
string grief = EventLabelBeat(label, d);
if (!string.IsNullOrEmpty(grief))
{
return grief;
}
if (id.IndexOf("child", System.StringComparison.OrdinalIgnoreCase) >= 0)
{
return "Mourning a child";
}
if (id.IndexOf("lover", System.StringComparison.OrdinalIgnoreCase) >= 0)
{
return "Mourning a lover";
}
return "Grief";
}
string happy = EventLabelBeat(label, d);
return happy ?? "";
}
private static string SceneLabelBeat(InterestCandidate scene, UnitDossier d)
{
if (scene == null || string.IsNullOrEmpty(scene.Label))
{
return "";
}
return EventLabelBeat(scene.Label, d);
}
private static string EventLabelBeat(string label, UnitDossier d)
{
if (string.IsNullOrEmpty(label))
{
return "";
}
string s = label.Trim();
if (s.StartsWith("Watching [", System.StringComparison.OrdinalIgnoreCase))
{
int close = s.IndexOf(']');
if (close > 0 && close + 1 < s.Length)
{
s = s.Substring(close + 1).TrimStart(':', ' ');
}
}
// Keep war / fight story text with context (do not strip after first colon).
if (s.StartsWith("War", System.StringComparison.OrdinalIgnoreCase)
|| s.StartsWith("Fighting", System.StringComparison.OrdinalIgnoreCase)
|| s.StartsWith("Battle", System.StringComparison.OrdinalIgnoreCase)
|| s.StartsWith("Clash", System.StringComparison.OrdinalIgnoreCase)
|| s.StartsWith("Grief", System.StringComparison.OrdinalIgnoreCase)
|| s.StartsWith("Mourn", System.StringComparison.OrdinalIgnoreCase)
|| s.StartsWith("Slain", System.StringComparison.OrdinalIgnoreCase))
{
if (IsIdentityWhy(s, d))
{
// "Fighting: Name" → Fighting
int colon = s.IndexOf(':');
if (colon > 0)
{
string head = s.Substring(0, colon).Trim();
return string.IsNullOrEmpty(head) ? "Fighting" : head;
}
return "";
}
return CleanBeat(s);
}
string shortened = ShortenWatchLabel(s, d);
if (string.IsNullOrEmpty(shortened) || IsIdentityWhy(shortened, d))
{
return "";
}
// Reject weak singleton/species crumbs ("Lone beetle").
if (IsWeakFlavorBeat(shortened, d))
{
return "";
}
return shortened;
}
private static string ActivityBeat(Actor actor)
{
if (actor == null)
{
return "";
}
// Task/job already live on the nametag; only surface hot action here.
ActivityBand band = ActivityInterestTable.Classify(actor);
if (band == ActivityBand.Hot)
{
return "In action";
}
return "";
}
private static bool IsWeakFlavorBeat(string text, UnitDossier d)
{
if (string.IsNullOrEmpty(text))
{
return true;
}
string t = text.Trim();
if (t.StartsWith("Lone ", System.StringComparison.OrdinalIgnoreCase)
|| t.Equals("Lone of kind", System.StringComparison.OrdinalIgnoreCase)
|| t.Equals("lone of kind", System.StringComparison.OrdinalIgnoreCase))
{
return true;
}
if (d != null
&& !string.IsNullOrEmpty(d.SpeciesId)
&& t.IndexOf(d.SpeciesId, System.StringComparison.OrdinalIgnoreCase) >= 0
&& t.IndexOf("Fight", System.StringComparison.OrdinalIgnoreCase) < 0
&& t.IndexOf("War", System.StringComparison.OrdinalIgnoreCase) < 0)
{
return true;
}
return false;
}
private static string CleanBeat(string text)
{
return string.IsNullOrEmpty(text) ? "" : text.Trim();
}
private static bool IsIdentityWhy(string text, UnitDossier d)
{
if (string.IsNullOrEmpty(text))
{
return true;
}
string t = text.Trim();
if (t.StartsWith("King ", System.StringComparison.OrdinalIgnoreCase)
|| t.StartsWith("King of ", System.StringComparison.OrdinalIgnoreCase)
|| t.StartsWith("Leader ", System.StringComparison.OrdinalIgnoreCase)
|| t.StartsWith("Leader of ", System.StringComparison.OrdinalIgnoreCase)
|| t.StartsWith("Favorite", System.StringComparison.OrdinalIgnoreCase))
{
return true;
}
// Pure title leftovers after ShortenWatchLabel ("King", "Leader", "Favorite").
if (t.Equals("King", System.StringComparison.OrdinalIgnoreCase)
|| t.Equals("Leader", System.StringComparison.OrdinalIgnoreCase)
|| t.Equals("Favorite", System.StringComparison.OrdinalIgnoreCase))
{
return true;
}
if (d != null
&& !string.IsNullOrEmpty(d.Name)
&& t.IndexOf(d.Name, System.StringComparison.OrdinalIgnoreCase) >= 0
&& t.IndexOf("Fight", System.StringComparison.OrdinalIgnoreCase) < 0
&& t.IndexOf("War", System.StringComparison.OrdinalIgnoreCase) < 0
&& t.IndexOf("mourn", System.StringComparison.OrdinalIgnoreCase) < 0
&& t.IndexOf("New species", System.StringComparison.OrdinalIgnoreCase) < 0)
{
// "Isemward (human, 12 kills)" style still useful via kills - keep if kills mentioned.
if (t.IndexOf("kill", System.StringComparison.OrdinalIgnoreCase) >= 0)
{
return false;
}
return true;
}
return IsRedundantWithHeadline(t, d);
}
private static bool IsShownTraitName(UnitDossier d, string text)
{
if (d == null || string.IsNullOrEmpty(text))
{
return false;
}
for (int i = 0; i < d.TopTraits.Count; i++)
{
TraitChip chip = d.TopTraits[i];
if (chip == null)
{
continue;
}
if ((!string.IsNullOrEmpty(chip.Name)
&& string.Equals(text, chip.Name, System.StringComparison.OrdinalIgnoreCase))
|| (!string.IsNullOrEmpty(chip.Id)
&& string.Equals(text, Humanize(chip.Id), System.StringComparison.OrdinalIgnoreCase)))
{
return true;
}
}
return false;
}
private static bool IsRedundantWithHeadline(string text, UnitDossier d)
{
if (string.IsNullOrEmpty(text) || d == null)
{
return true;
}
if (string.Equals(text, d.Headline, System.StringComparison.OrdinalIgnoreCase))
{
return true;
}
if (string.Equals(text, d.Name, System.StringComparison.OrdinalIgnoreCase)
|| string.Equals(text, d.SpeciesId, System.StringComparison.OrdinalIgnoreCase))
{
return true;
}
// "Name (species)" variants already covered by headline.
if (!string.IsNullOrEmpty(d.Name) && !string.IsNullOrEmpty(d.SpeciesId)
&& text.IndexOf(d.Name, System.StringComparison.OrdinalIgnoreCase) >= 0
&& text.IndexOf(d.SpeciesId, System.StringComparison.OrdinalIgnoreCase) >= 0)
{
return true;
}
return false;
}
private static string ShortenWatchLabel(string label, UnitDossier d)
{
if (string.IsNullOrEmpty(label))
{
return "";
}
string s = label.Trim();
if (s.StartsWith("New species:", System.StringComparison.OrdinalIgnoreCase))
{
return "New species";
}
if (s.StartsWith("Watching [", System.StringComparison.OrdinalIgnoreCase))
{
int close = s.IndexOf(']');
if (close > 0 && close + 1 < s.Length)
{
s = s.Substring(close + 1).TrimStart(':', ' ');
}
}
// Drop trailing "Name" duplicates like "Lone wolf: Waf" -> keep short form.
int colon = s.IndexOf(':');
if (colon > 0 && colon < 28)
{
s = s.Substring(0, colon).Trim();
}
if (d != null && IsRedundantWithHeadline(s, d))
{
return "";
}
return s;
}
private static void CollectReasons(UnitDossier d, Actor actor, Dictionary<string, int> speciesCounts)
{
if (d.IsFavorite)
{
d.ScoreReasons.Add("favorite");
}
if (d.IsKing)
{
d.ScoreReasons.Add("king");
}
else if (d.IsCityLeader)
{
d.ScoreReasons.Add("leader");
}
else if (actor.is_profession_warrior)
{
d.ScoreReasons.Add("warrior");
}
if (d.IsFighting)
{
d.ScoreReasons.Add("fighting");
}
if (d.Kills >= 100)
{
d.ScoreReasons.Add("100+ kills");
}
else if (d.Kills >= 50)
{
d.ScoreReasons.Add("50+ kills");
}
else if (d.Kills >= 10)
{
d.ScoreReasons.Add("10+ kills");
}
if (d.IsLoneSpecies)
{
d.ScoreReasons.Add("lone of kind");
}
if (d.IsSpectacle)
{
d.ScoreReasons.Add("spectacle");
}
if (d.Renown >= 100)
{
d.ScoreReasons.Add("renown " + d.Renown);
}
// Trait names are shown as dossier chips; keep ScoreReasons for non-trait flavor only.
}
private static string BuildDetailLine(UnitDossier d)
{
// Compact text fallback for logs/harness: task + optional kingdom.
StringBuilder sb = new StringBuilder();
if (!string.IsNullOrEmpty(d.TaskText))
{
sb.Append(d.TaskText);
}
if (!string.IsNullOrEmpty(d.KingdomName)
&& !string.Equals(d.KingdomName, d.SpeciesId, System.StringComparison.OrdinalIgnoreCase)
&& !string.Equals(d.KingdomName, d.Name, System.StringComparison.OrdinalIgnoreCase))
{
if (sb.Length > 0)
{
sb.Append(" · ");
}
sb.Append(d.KingdomName);
}
else if (!string.IsNullOrEmpty(d.CityName)
&& !string.Equals(d.CityName, d.SpeciesId, System.StringComparison.OrdinalIgnoreCase)
&& !string.Equals(d.CityName, d.Name, System.StringComparison.OrdinalIgnoreCase))
{
if (sb.Length > 0)
{
sb.Append(" · ");
}
sb.Append(d.CityName);
}
return sb.ToString();
}
private static void FillSex(UnitDossier d, Actor actor)
{
try
{
d.IsMale = actor.isSexMale();
d.IsFemale = actor.isSexFemale();
}
catch
{
d.IsMale = false;
d.IsFemale = false;
}
}
private static void FillTopTraits(UnitDossier d, Actor actor)
{
d.TopTraits.Clear();
try
{
if (!actor.hasTraits())
{
return;
}
// Pick the most interesting traits: rarity first, then non-default / dramatic.
List<ActorTrait> ranked = new List<ActorTrait>();
foreach (ActorTrait trait in actor.getTraits())
{
if (trait == null || string.IsNullOrEmpty(trait.id))
{
continue;
}
ranked.Add(trait);
}
ranked.Sort((a, b) => TraitInterestScore(b, actor).CompareTo(TraitInterestScore(a, actor)));
for (int i = 0; i < ranked.Count && d.TopTraits.Count < 3; i++)
{
ActorTrait trait = ranked[i];
d.TopTraits.Add(new TraitChip
{
Id = trait.id,
Name = TraitDisplayName(trait),
Trait = trait
});
}
}
catch
{
// ignore trait access failures on exotic units
}
}
/// <summary>
/// Spectator-facing interest: Legendary/Epic over Normal, non-default over species baseline,
/// Positive/Negative over bland Other. Ties break on rarer birth rates.
/// </summary>
private static int TraitInterestScore(ActorTrait trait, Actor actor)
{
if (trait == null)
{
return int.MinValue;
}
int score = 0;
try
{
score += (int)trait.rarity * 100;
}
catch
{
// older builds without rarity
}
try
{
if (trait.type == TraitType.Negative)
{
score += 25;
}
else if (trait.type == TraitType.Positive)
{
score += 15;
}
}
catch
{
// ignore
}
if (!IsDefaultTraitForActor(trait, actor))
{
score += 50;
}
else
{
score -= 40;
}
try
{
// Low birth rate = unusual to spawn with; more interesting on a watch card.
if (trait.rate_birth > 0 && trait.rate_birth <= 5)
{
score += 20;
}
else if (trait.rate_birth >= 40)
{
score -= 15;
}
}
catch
{
// ignore
}
string id = trait.id ?? "";
if (id.IndexOf("miracle", StringComparison.OrdinalIgnoreCase) >= 0
|| id.IndexOf("immortal", StringComparison.OrdinalIgnoreCase) >= 0
|| id.IndexOf("zombie", StringComparison.OrdinalIgnoreCase) >= 0
|| id.IndexOf("plague", StringComparison.OrdinalIgnoreCase) >= 0
|| id.IndexOf("blessed", StringComparison.OrdinalIgnoreCase) >= 0
|| id.IndexOf("cursed", StringComparison.OrdinalIgnoreCase) >= 0
|| id.IndexOf("king", StringComparison.OrdinalIgnoreCase) >= 0
|| id.IndexOf("evil", StringComparison.OrdinalIgnoreCase) >= 0)
{
score += 35;
}
return score;
}
private static bool IsDefaultTraitForActor(ActorTrait trait, Actor actor)
{
try
{
if (trait == null || actor == null || actor.asset == null
|| trait.default_for_actor_assets == null)
{
return false;
}
return trait.default_for_actor_assets.Contains(actor.asset);
}
catch
{
return false;
}
}
private static string TraitDisplayName(ActorTrait trait)
{
try
{
string locale = trait.getLocaleID();
if (!string.IsNullOrEmpty(locale))
{
string localized = LocalizedTextManager.getText(locale);
if (!string.IsNullOrEmpty(localized) && localized != locale)
{
return localized;
}
}
}
catch
{
// fall through
}
return Humanize(trait.id);
}
/// <summary>Live AI task label for the nametag chip (localized, else humanized id).</summary>
public static string ReadLiveTask(Actor actor)
{
return SafeTask(actor);
}
private static string SafeTask(Actor actor)
{
try
{
if (actor == null || !actor.hasTask() || actor.ai == null || actor.ai.task == null)
{
return "";
}
string text = actor.ai.task.getLocalizedText();
if (!string.IsNullOrEmpty(text) && text != "-")
{
return text;
}
return Humanize(actor.ai.task.id);
}
catch
{
try
{
if (actor != null && actor.hasTask() && actor.ai?.task != null)
{
return Humanize(actor.ai.task.id);
}
}
catch
{
// ignore
}
return "";
}
}
private static string Humanize(string raw)
{
if (string.IsNullOrEmpty(raw))
{
return "";
}
return raw.Replace('_', ' ');
}
private static string SafeName(Actor actor)
{
try
{
string name = actor.getName();
if (!string.IsNullOrEmpty(name))
{
return name;
}
}
catch
{
// fall through
}
return actor.asset != null ? actor.asset.id : "unit";
}
private static int SafeAge(Actor actor)
{
try
{
return actor.getAge();
}
catch
{
return 0;
}
}
private static int SafeLevel(Actor actor)
{
try
{
return actor.data != null ? actor.data.level : 0;
}
catch
{
return 0;
}
}
}