NominaOcculta/NominaOcculta/NameRepository.cs

147 lines
5.4 KiB
C#
Executable File

using System;
using System.Collections.Generic;
using System.Linq;
using Dalamud.Game;
using Lumina.Excel.GeneratedSheets;
namespace NominaOcculta {
internal class NameRepository : IDisposable {
private Plugin Plugin { get; }
private Random Rng { get; } = new();
private Dictionary<(byte, byte, byte), Queue<string>> Names { get; } = new();
private Dictionary<string, string> Replacements { get; } = new();
private Dictionary<string, (byte, byte, byte)?> LastSeenInfo { get; } = new();
private readonly int _numRaces;
internal bool Initialised;
internal NameRepository(Plugin plugin) {
this.Plugin = plugin;
this._numRaces = this.Plugin.DataManager.GetExcelSheet<Race>()!.Count(row => row.RowId != 0);
this.Plugin.Functions.LoadSheet("CharaMakeName");
this.Plugin.ClientState.Login += this.OnLogin;
for (var race = (byte) 1; race <= this._numRaces; race++) {
for (var clan = (byte) 0; clan <= 1; clan++) {
for (var sex = (byte) 0; sex <= 1; sex++) {
this.Names[(race, clan, sex)] = new Queue<string>();
}
}
}
this.Plugin.Framework.Update += this.OnFrameworkUpdate;
}
public void Dispose() {
this.Plugin.Framework.Update -= this.OnFrameworkUpdate;
this.Plugin.ClientState.Login -= this.OnLogin;
}
private void OnFrameworkUpdate(Framework framework) {
// The in-game name generator will generate duplicate names if it is given
// identical parameters on the same frame. Instead, we will fill up a queue
// with 100 names (the maximum amount of players in the object table) for
// each combination of parameters, generating one name per combination per
// frame.
for (var race = (byte) 1; race <= this._numRaces; race++) {
for (var clan = (byte) 0; clan <= 1; clan++) {
for (var sex = (byte) 0; sex <= 1; sex++) {
var queue = this.Names[(race, clan, sex)];
if (queue.Count >= 100) {
this.Initialised = true;
continue;
}
var name = this.Plugin.Functions.GenerateName(race, clan, sex);
if (name != null && (!queue.TryPeek(out var peek) || peek != name)) {
queue.Enqueue(name);
}
}
}
}
}
private void OnLogin(object? sender, EventArgs e) {
// The game unloads the CharaMakeName sheet after logging in.
// We need this sheet to generate names, so we load it again.
this.Plugin.Functions.LoadSheet("CharaMakeName");
}
/// <summary>
/// <para>
/// Get a consistent replacement name for a real name.
/// </para>
/// <para>
/// This will generate a new name if the given info changes.
/// </para>
/// </summary>
/// <param name="name"></param>
/// <param name="info">(race, clan, sex) if known. Any unknowns should be 0xFF to be replaced with random, valid values.</param>
/// <returns>A replacement name. Returns null if name is null/empty or no name could be generated.</returns>
internal string? GetReplacement(string name, (byte race, byte clan, byte sex) info) {
if (string.IsNullOrEmpty(name)) {
return null;
}
if (this.LastSeenInfo.TryGetValue(name, out var lastInfo) && lastInfo != info) {
this.Replacements.Remove(name);
}
this.LastSeenInfo[name] = info;
if (this.Replacements.TryGetValue(name, out var replacement)) {
return replacement;
}
// need to generate a name after this point
// use random parameters for info if none was specified
if (info.race == 0xFF) {
info.race = (byte) this.Rng.Next(1, this._numRaces + 1);
}
if (info.clan == 0xFF) {
info.clan = (byte) this.Rng.Next(0, 2);
}
if (info.sex == 0xFF) {
info.clan = (byte) this.Rng.Next(0, 2);
}
// get a name for the given info if possible
if (this.Names.TryGetValue(info, out var names)) {
// make sure the new name is not the same as the old name
names.TryDequeue(out var newName);
while (newName == name) {
names.TryDequeue(out newName);
}
if (newName != null) {
this.Replacements[name] = newName;
return newName;
}
}
// otherwise, get a random name
// can't really do anything about conflicts here, but this should be a very rare/impossible case
var random = this.Plugin.Functions.GenerateName(info.race, info.clan, info.sex);
if (random != null) {
this.Replacements[name] = random;
}
return random;
}
internal void Reset() {
this.Replacements.Clear();
}
}
}