C#:
using System;
using System.Reflection;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
using Oxide.Core;
using Oxide.Core.Plugins;
using Oxide.Core.Configuration;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Facepunch;
using Rust;
using Rust.Ai;
using HarmonyLib;
#region Changelogs and ToDo
/**********************************************************************
*
* 1.0.0 : - Initial release
* 1.0.1 : - Added a initialise check to avoid nre when renaming unfinished npc spawns
* - Added Update active npc when plugin is loaded
* - Added name Buildup to config
* 1.0.2 : - Added API : string GiveName(global::HumanNPC npc, string Title,bool UseName,bool UseLetter)
*
* if (BetterNpcNames != null) npc.displayName = (BetterNpcNames.Call<string>("GiveName",npc , configData.NpcTitle, true, true));
* 1.0.3 : - Added Corpse loot from npc inventory,belt,wearables
* - Added options to clear belt or wearables when populating the corpse
* - Fixed nullreference issue
* - Added check at serverstartup
* - Added API : string GiveName(ScarecrowNPC npc, string Title,bool UseName,bool UseLetter)
* 1.1.0 - Patched for Rust Changes
* 1.2.0 - Harmony Patched for Rust Changes
* - Added Interface.CallHook("CanBetterNpcNamesRename" , npc);//to add avoid BetterNpcNames to edit the npc names
* 1.2.1 - Null check added on Letter randomising
* 1.2.2 - Moved random lettering on init for the harmonypatching
* 1.2.3 - Added support for GingerbreadNPC
* - Other fixes
* 1.2.4 - Hotfix
* 1.2.5 - Wrapped console logging under a debug (true/false in cfg)
* - Fixed the forced extra spacing before the random letter.
* - Removed Linq usage.
* - Added individual scientisttype titles with default fallback title
*
**********************************************************************/
#endregion
namespace Oxide.Plugins
{
[Info("Better NPC Names" , "Krungh Crow" , "1.2.5")]
[Description("Names the unnamed npc and fixes corpse displaynames")]
class BetterNpcNames : RustPlugin
{
#region Variables
string npcname;
System.Random Rand;
bool IsReady;
int NamingCount = 0;
public static BetterNpcNames Instance { get; private set; }
private bool Debug;
#endregion
#region Harmony
private Harmony harmony;
[HarmonyPatch(typeof(ScientistNPC))]
[HarmonyPatch("displayName" , MethodType.Getter)]
public static class ScientistNPC_DisplayName
{
static bool Prefix(ref string __result , ScientistNPC __instance)
{
if (__instance == null) return true;
// Skip NPCs other plugins want to control
if (BetterNpcNames.Instance.NpcList.Contains(__instance.userID))
return true;
var plugin = BetterNpcNames.Instance;
string dynamicTitle = plugin.GetTitleForNPC(__instance);
var method = typeof(BetterNpcNames).GetMethod(
"GiveName" ,
BindingFlags.NonPublic | BindingFlags.Instance ,
null ,
new[] { typeof(ScientistNPC) , typeof(string) , typeof(bool) , typeof(bool) } ,
null
);
if (method != null)
{
__result = (string)method.Invoke(plugin , new object[] { __instance , dynamicTitle , true , true });
return false;
}
return true;
}
}
[HarmonyPatch(typeof(UnderwaterDweller))]
[HarmonyPatch("displayName" , MethodType.Getter)]
public static class UnderwaterDweller_DisplayName
{
static bool Prefix(ref string __result , UnderwaterDweller __instance)
{
string npcname = "Underwater Dweller";
var _RandomName = RandomUsernames.Get(__instance.userID.Get());
string RandomName = MakeCap(_RandomName);
string Title = udTitle;
string letter = BetterNpcNames.Instance.Letter();
npcname = sNaming.Replace("{Title}" , Title).Replace("{RandomName}" , RandomName).Replace("{RandomLetter}" , letter);
__result = npcname;
return false;
}
}
[HarmonyPatch(typeof(TunnelDweller))]
[HarmonyPatch("displayName" , MethodType.Getter)]
public static class TunnelDweller_DisplayName
{
static bool Prefix(ref string __result , TunnelDweller __instance)
{
string npcname = "Tunnel Dweller";
var _RandomName = RandomUsernames.Get(__instance.userID.Get());
string RandomName = MakeCap(_RandomName);
string Title = tdTitle;
string letter = BetterNpcNames.Instance.Letter();
npcname = sNaming.Replace("{Title}" , Title).Replace("{RandomName}" , RandomName).Replace("{RandomLetter}" , letter);
__result = npcname;
return false;
}
}
#endregion
#region Configuration
void Init()
{
Instance = this;
harmony = new Harmony("krunghcrow.better.npc.names");
harmony.PatchAll();
Rand = new System.Random();
if (!LoadConfigVariables())
{
Puts("Config file issue detected. Please delete file, or check syntax and fix.");
return;
}
sTitle = configData.Titles.Sci;
udTitle = configData.Titles.UDwel;
tdTitle = configData.Titles.TDwel;
Debug = configData.Debug;
}
void Unload()
{
harmony.UnpatchAll(harmony.Id);
}
public static string sTitle = "";
public static string udTitle = "";
public static string tdTitle = "";
public static string sNaming = "";
private ConfigData configData;
class ConfigData
{
[JsonProperty(PropertyName = "Use Debug")]
public bool Debug = false;
[JsonProperty(PropertyName = "Name the npcplayertest")]
public bool UseTest = false;
[JsonProperty(PropertyName = "Npc Titles")]
public Titles Titles = new Titles();
[JsonProperty(PropertyName = "Name Buildup")]
public string Naming = "{Title} {RandomName} {RandomLetter}";
[JsonProperty(PropertyName = "Corpse setup")]
public CorpseData Corpse = new CorpseData();
}
class CorpseData
{
[JsonProperty(PropertyName = "Clear Deafault loot")]
public bool ClDefault = false;
[JsonProperty(PropertyName = "Clear Belt")]
public bool ClBelt = false;
[JsonProperty(PropertyName = "Clear Clothes")]
public bool ClClothes = false;
}
class Titles
{
[JsonProperty(PropertyName = "Frankenstein")]
public string Frank = "Frankenstein";
[JsonProperty(PropertyName = "Scarecrow")]
public string Crow = "Scarecrow";
[JsonProperty(PropertyName = "Scientist (Global fallback)")]
public string Sci = "Dr";
[JsonProperty(PropertyName = "Scientists individual")]
public Dictionary<string , string> PrefabOverrides = new Dictionary<string , string>
{
{ "scientistnpc_arena", "Gladiator"},
{ "scientistnpc_bradley", "Guard"},
{ "scientistnpc_bradley_heavy", "Heavy Guard"},
{ "scientistnpc_cargo", "Cargo Sailor"},
{ "scientistnpc_cargo_turret_any", "Cargo Officer"},
{ "scientistnpc_cargo_turret_lr300", "Cargo Guard"},
{ "scientistnpc_ch47_gunner", "Airman"},
{ "scientistnpc_excavator", "Digger"},
{ "scientistnpc_full_any", "Rougue" },
{ "scientistnpc_full_lr300", "Riflemen" },
{ "scientistnpc_full_mp5", "Trooper" },
{ "scientistnpc_full_pistol", "Gunman" },
{ "scientistnpc_full_shotgun", "Gunner" },
{ "scientistnpc_heavy", "Fatty" },
{ "scientistnpc_junkpile_pistol", "Junker" },
{ "scientistnpc_oilrig", "Oiler" },
{ "scientistnpc_outbreak", "Investigator" },
{ "scientistnpc_patrol", "Patroller" },
{ "scientistnpc_patrol_arctic", "Arctic" },
{ "scientistnpc_peacekeeper", "Peacekeeper" },
{ "scientistnpc_ptboat", "Sailor" },
{ "scientistnpc_rhib", "Frogmen" },
{ "scientistnpc_roam", "Roamer" },
{ "scientistnpc_roam_nvg_variant", "Nightswatch" },
{ "scientistnpc_roamtethered", "Tethered" },
};
[JsonProperty(PropertyName = "UnderwaterDweller")]
public string UDwel = "UnderwaterDweller";
[JsonProperty(PropertyName = "TunnelDweller")]
public string TDwel = "TunnelDweller";
[JsonProperty(PropertyName = "GingerbreadNPC")]
public string Ginger = "Ginger";
[JsonProperty(PropertyName = "npcplayertest")]
public string Test = "playertest";
}
private bool LoadConfigVariables()
{
try { configData = Config.ReadObject<ConfigData>(); }
catch { return false; }
SaveConf();
return true;
}
protected override void LoadDefaultConfig()
{
Puts("Fresh install detected Creating a new config file.");
configData = new ConfigData();
SaveConf();
}
void SaveConf() => Config.WriteObject(configData , true);
#endregion
#region external Hooks
List<ulong> NpcList = new List<ulong>();
void OnBotReSpawnNPCSpawned(ScientistNPC npc , string profilename , string group) => NpcList.Add(npc.userID);
void OnHuntsmanSpawn(ScientistNPC npc) => NpcList.Add(npc.userID);
void CanBetterNpcNamesRename(ScientistNPC npc) => NpcList.Add(npc.userID);
void OnEntityKill(ScientistNPC npc)
{
if (npc == null)
return;
if (NpcList.Contains(npc.userID))
NpcList.Remove(npc.userID);
}
#endregion
#region Oxide Hooks
void OnServerInitialized(bool initial)
{
Rand = new System.Random();
if (initial)
{
IsReady = false;
CheckInit();
return;
}
IsReady = true;
if (IsReady) Debugmsg("Server is Ready for NPC Naming");
NamingCount = 0;
sNaming = configData.Naming;
foreach (var baseNetworkable in BaseNetworkable.serverEntities)
{
var npc = baseNetworkable as global::NPCPlayer;
if (npc == null) continue;
string originalNaming = npc.displayName;
UpdateName(npc);
if (originalNaming != npc.displayName) { NamingCount++; }
}
if (NamingCount != 0) Debugmsg($"{NamingCount} NPC's where renamed");
}
void OnEntitySpawned(global::BaseEntity entity)
{
var npc = entity as NPCPlayer;
if (npc != null && IsReady)
{
try
{
UpdateName(npc);
}
catch (Exception ex)
{
}
}
return;
}
void OnEntityDeath(global::NPCPlayer npc , HitInfo info)
{
if (npc == null || info == null)
return;
ItemContainer[] source =
{
npc.inventory?.containerMain,
npc.inventory?.containerWear,
npc.inventory?.containerBelt
};
var npcInv = new Inv
{
name = npc.displayName
};
NpcContents[npc.userID] = npcInv;
for (int i = 0; i < source.Length; i++)
{
var container = source[i];
if (container == null)
continue;
foreach (var item in container.itemList)
{
npcInv.inventory[i].Add(new NpcInventory
{
ID = item.info.itemid ,
amount = item.amount ,
skinID = item.skin
});
}
}
}
void OnEntitySpawned(NPCPlayerCorpse corpse)
{
if (corpse == null || corpse.IsDestroyed || !IsReady)
return;
try
{
ulong id = corpse.playerSteamID;
if (!NpcContents.TryGetValue(id , out var npcInv))
return;
corpse.playerName = OverrideCorpseName(npcInv.name);
if (corpse.ShortPrefabName.Contains("ginger" , StringComparison.OrdinalIgnoreCase))
return;
timer.Once(0.1f , () =>
{
if (corpse == null || corpse.IsDestroyed || !IsReady)
return;
try
{
var dispenser = corpse.GetComponent<ResourceDispenser>();
if (dispenser != null)
{
dispenser.containedItems.Add(new ItemAmount
{
itemDef = ItemManager.FindItemDefinition("skull.human") ,
amount = 1 ,
startAmount = 1
});
dispenser.containedItems.Add(new ItemAmount
{
itemDef = ItemManager.FindItemDefinition("humanmeat.raw") ,
amount = 6 ,
startAmount = 1
});
dispenser.containedItems.Add(new ItemAmount
{
itemDef = ItemManager.FindItemDefinition("fat.animal") ,
amount = 2 ,
startAmount = 1
});
dispenser.Initialize();
}
for (int i = 0; i < npcInv.inventory.Length && i < corpse.containers.Length; i++)
{
var container = corpse.containers[i];
if (container == null)
continue;
foreach (var item in npcInv.inventory[i])
{
var giveItem = ItemManager.CreateByItemID(item.ID , item.amount , item.skinID);
if (!giveItem.MoveToContainer(container , -1 , true))
giveItem.Remove();
}
}
if (configData.Corpse.ClDefault && corpse.containers.Length > 0)
corpse.containers[0].Clear();
if (configData.Corpse.ClClothes && corpse.containers.Length > 1)
corpse.containers[1].Clear();
if (configData.Corpse.ClBelt && corpse.containers.Length > 2)
corpse.containers[2].Clear();
timer.Once(5f , () => NpcContents.Remove(id));
}
catch
{
}
});
}
catch
{
}
}
protected virtual string OverrideCorpseName(string _playerName) { return _playerName; }
#endregion
#region API
private string GiveName(global::HumanNPC npc , string Title , bool UseName , bool UseLetter)
{
string RandomName = string.Empty;
string RLetter = string.Empty;
if (UseName)
{
string _RandomName = RandomUsernames.Get(npc.userID.Get());
RandomName = MakeCap(_RandomName);
}
if (UseLetter) RLetter = Letter();
npcname = (configData.Naming).Replace("{Title}" , Title).Replace("{RandomName}" , RandomName).Replace("{RandomLetter}" , RLetter);
return npc.displayName = npcname;
}
private string GiveName(ScarecrowNPC npc , string Title , bool UseName , bool UseLetter)
{
string RandomName = string.Empty;
string RLetter = string.Empty;
if (UseName)
{
string _RandomName = RandomUsernames.Get(npc.userID.Get()); RandomName = MakeCap(_RandomName);
}
if (UseLetter) RLetter = Letter();
npcname = (configData.Naming).Replace("{Title}" , Title).Replace("{RandomName}" , RandomName).Replace("{RandomLetter}" , RLetter);
return npc.displayName = npcname;
}
#endregion
#region Helpers Corpse
public Dictionary<ulong , Inv> NpcContents = new Dictionary<ulong , Inv>();
public class Inv
{
public string name;
public List<NpcInventory>[] inventory = { new List<NpcInventory>() , new List<NpcInventory>() , new List<NpcInventory>() };
}
public class NpcInventory
{
public int ID;
public int amount;
public ulong skinID;
}
#endregion
#region Helpers Naming
private string GetTitleForNPC(NPCPlayer npc)
{
string fallback = configData?.Titles?.Sci ?? "Scientist";
if (npc == null) return fallback;
string prefab = npc.ShortPrefabName;
if (string.IsNullOrEmpty(prefab)) return fallback;
var overrides = configData?.Titles?.PrefabOverrides;
if (overrides == null || overrides.Count == 0) return fallback;
// Exact match only
foreach (var kvp in overrides)
{
if (string.Equals(prefab , kvp.Key , StringComparison.OrdinalIgnoreCase))
{
if (!string.IsNullOrWhiteSpace(kvp.Value))
return kvp.Value;
return fallback;
}
}
return fallback;
}
string Letter()//Generates random letter
{
if (Rand == null)
{
Rand = new System.Random();
}
int num = Rand.Next(0 , 26);
char x = (char)('a' + num);
return x.ToString().ToUpper();
}
public static string MakeCap(string name) //makes first letter of npc name Uppercase
{
if (string.IsNullOrEmpty(name)) return string.Empty;
char[] a = name.ToCharArray();
a[0] = char.ToUpper(a[0]);
return new string(a);
}
void UpdateName(global::NPCPlayer npc) //Names the Unnamed npc
{
if (Interface.CallHook("OnNpcNaming" , npc.userID) != null || Interface.CallHook("OnNpcNaming" , npc) != null)
{
Debugmsg("Test trigger for OnNpcNaming");
}
if (NpcList.Contains(npc.userID))
{
Debugmsg($"Skipping BotResSpawn npc {npc.userID}"); //example
return;
}
try
{
npcname = npc.displayName.ToString();
if (npcname == npc.userID.ToString() || npc.ShortPrefabName.Contains("ginger"))
{
var _RandomName = RandomUsernames.Get(npc.userID.Get());
string RandomName = MakeCap(_RandomName);
string Title = string.Empty;
switch (npc)
{
case ScientistNPC:
{
string prefab = npc.ShortPrefabName;
if (prefab != null && prefab.Contains("scientistnpc_heavy" , StringComparison.OrdinalIgnoreCase))
Title = "Fatty";
else
Title = configData.Titles.Sci;
break;
}
case ScarecrowNPC: Title = configData.Titles.Crow; break;
case FrankensteinPet: Title = configData.Titles.Frank; break;
case UnderwaterDweller: Title = configData.Titles.UDwel; break;
case TunnelDweller: Title = configData.Titles.TDwel; break;
case GingerbreadNPC: Title = configData.Titles.Ginger; break;
default:
if (npc.ShortPrefabName.Contains("playertest"))
{
if (!configData.UseTest) return;
Title = configData.Titles.Test;
}
else
{
Debugmsg($"Skipping unlisted NPC type: {npc.ShortPrefabName} ({npc.userID})");
return;
}
break;
}
npcname = configData.Naming
.Replace("{Title}" , Title)
.Replace("{RandomName}" , RandomName)
.Replace("{RandomLetter}" , Letter());
npc.displayName = npcname;
}
}
catch (Exception ex)
{
Debugmsg($"[BetterNpcNames] Exception caught during NPC name update for {npc?.ShortPrefabName ?? "Unknown"}: {ex.Message}");
}
}
#endregion
#region Helpers Various
void CheckInit()//checks if server is fully initialised
{
timer.Once(10f , () =>
{
if (Rust.Application.isLoading)
{
Debugmsg($"Server still loading....");
CheckInit();
return;
}
IsReady = true;
Debugmsg($"Server is Ready for NPC Naming....");
});
}
void Debugmsg(string key)
{
if (Debug) Puts($"[Debug] {key}");
}
#endregion
}
}