Files
SPT-Server-Build/Tools/ItemTplGenerator/ItemTplGenerator.cs
T
2025-01-21 22:57:13 +00:00

569 lines
19 KiB
C#

using Core.Callbacks;
using Core.DI;
using Core.Helpers;
using Core.Models.Eft.Common.Tables;
using Core.Models.Enums;
using Core.Models.Utils;
using Core.Servers;
using Core.Services;
using SptCommon.Annotations;
using SptCommon.Extensions;
using Path = System.IO.Path;
namespace ItemTplGenerator;
[Injectable]
public class ItemTplGenerator(
ISptLogger<ItemTplGenerator> _logger,
DatabaseServer _databaseServer,
LocaleService _localeService,
ItemHelper _itemHelper,
// @inject("FileSystemSync") protected fileSystemSync: FileSystemSync,
IEnumerable<OnLoad> _onLoadComponents
)
{
private string enumDir;
private Dictionary<string, TemplateItem> items;
private IDictionary<string, string> itemOverrides;
private HashSet<string> collidedEnumKeys = [];
public async Task Run()
{
itemOverrides = ItemOverrides.ItemOverridesDictionary;
// Load all of the onload components, this gives us access to most of SPTs injections
foreach (var onLoad in _onLoadComponents)
{
if (onLoad is HttpCallbacks)
continue;
await onLoad.OnLoad();
}
// Figure out our source and target directories
var projectDir = Directory.GetParent("./").Parent.Parent.Parent.Parent.Parent;
enumDir = Path.Combine(projectDir.FullName, "Libraries", "Core", "Models", "Enums");
items = _databaseServer.GetTables().Templates.Items;
// Generate an object containing all item name to ID associations
var orderedItemsObject = GenerateItemsObject();
// Log any changes to enum values, so the source can be updated as required
LogEnumValueChanges(orderedItemsObject, "ItemTpl", typeof(ItemTpl));
var itemTplOutPath = Path.Combine(enumDir, "ItemTpl.cs");
WriteEnumsToFile(
itemTplOutPath,
new Dictionary<string, Dictionary<string, string>>() { { nameof(ItemTpl), orderedItemsObject } }
);
// Handle the weapon type enums
var weaponsObject = GenerateWeaponsObject();
LogEnumValueChanges(weaponsObject, "Weapons", typeof(Weapons));
var weaponTypeOutPath = Path.Combine(enumDir, "Weapons.cs");
WriteEnumsToFile(
weaponTypeOutPath,
new Dictionary<string, Dictionary<string, string>>() { { nameof(Weapons), weaponsObject } }
);
_logger.Info("Generating items finished");
}
/**
* Return an object containing all items in the game with a generated name
* @returns An object containing a generated item name to item ID association
*/
private Dictionary<string, string> GenerateItemsObject()
{
var itemsObject = new Dictionary<string, string>();
foreach (var item in items.Values)
{
// Skip invalid items (Non-Item types, and shrapnel)
if (!IsValidItem(item)) continue;
var itemParentName = GetParentName(item);
var itemPrefix = GetItemPrefix(item);
var itemName = GetItemName(item);
var itemSuffix = GetItemSuffix(item);
// Handle the case where the item starts with the parent category name. Avoids things like 'POCKETS_POCKETS'
if (itemName.Length > itemParentName.Length &&
itemParentName == itemName.Substring(1, itemParentName.Length) &&
itemPrefix == "")
{
itemName = itemName.Substring(itemParentName.Length + 1);
if (itemName.Length > 0 && itemName[0] != '_')
{
itemName = $"_{itemName}";
}
}
// Handle the case where the item ends with the parent category name. Avoids things like 'KEY_DORM_ROOM_103_KEY'
if (itemName.Length >= itemParentName.Length &&
itemParentName == itemName.Substring(itemName.Length - itemParentName.Length))
{
itemName = itemName.Substring(0, itemName.Length - itemParentName.Length);
if (itemName.Substring(itemName.Length - 1) == "_")
{
itemName = itemName.Substring(0, itemName.Length - 1);
}
}
var itemKey = $"{itemParentName}{itemPrefix}{itemName}{itemSuffix}";
// Strip out any remaining special characters
itemKey = SanitizeEnumKey(itemKey);
// If the key already exists, see if we can add a suffix to both this, and the existing conflicting item
if (itemsObject.ContainsKey(itemKey) || collidedEnumKeys.Contains(itemKey))
{
// Keep track, so we can handle 3+ conflicts
collidedEnumKeys.Add(itemKey);
var itemNameSuffix = GetItemNameSuffix(item);
if (!string.IsNullOrEmpty(itemNameSuffix))
{
// Try to update the old key reference if we haven't already
if (itemsObject.ContainsKey(itemKey))
{
var oldItemId = itemsObject[itemKey];
var oldItemNameSuffix = GetItemNameSuffix(items[oldItemId]);
if (!string.IsNullOrEmpty(oldItemNameSuffix))
{
var oldItemNewKey = SanitizeEnumKey($"{itemKey}_{oldItemNameSuffix}");
itemsObject.Remove(itemKey);
itemsObject[oldItemNewKey] = oldItemId;
}
}
itemKey = SanitizeEnumKey($"{itemKey}_{itemNameSuffix}");
// If we still collide, log an error
if (itemsObject.ContainsKey(itemKey))
{
_logger.Error(
$"After rename, itemsObject already contains {itemKey} {itemsObject[itemKey]} => {item.Id}"
);
}
}
else
{
var val = itemsObject.ContainsKey(itemKey) ?
itemsObject[itemKey] :
itemKey;
_logger.Error($"New itemOverride entry required: itemsObject already contains {itemKey} {val} => {item.Id}");
continue;
}
}
itemsObject[itemKey] = item.Id;
}
// Sort the items object
var itemList = itemsObject.ToList();
itemList.Sort((kv1, kv2) => kv1.Key.CompareTo(kv2.Key));
var orderedItemsObject = itemList.ToDictionary(kv => kv.Key, kv => kv.Value);
return orderedItemsObject;
}
/**
*
* @param orderedItemsObject The previously generated object of item name to item ID associations
* @returns
*/
private Dictionary<string, string> GenerateWeaponsObject()
{
var weaponsObject = new Dictionary<string, string>();
foreach (var kv /*[itemId, item]*/ in items)
{
if (!_itemHelper.IsOfBaseclass(kv.Key, BaseClasses.WEAPON))
{
continue;
}
var caliber = CleanCaliber(kv.Value.Properties.AmmoCaliber.ToUpper());
var weaponShortName = _localeService.GetLocaleDb()[$"{kv.Key} ShortName"]?.ToUpper();
// Special case for the weird duplicated grenade launcher
if (kv.Key == "639c3fbbd0446708ee622ee9")
{
weaponShortName = "FN40GL_2";
}
// Include any bracketed suffixes that exist, handles the case of colored gun variants
var weaponFullName = _localeService.GetLocaleDb()[$"{kv.Key} Name"]?.ToUpper();
if (weaponFullName.RegexMatch("\\((.+?)\\)$", out var itemNameBracketSuffix) &&
!weaponShortName.EndsWith(itemNameBracketSuffix.Groups[1].Value))
{
weaponShortName += $"_{itemNameBracketSuffix.Groups[1].Value}";
}
var parentName = GetParentName(kv.Value);
// Handle special characters
var weaponName = $"{parentName}_{caliber}_{weaponShortName}"
.RegexReplace("[- ]", "_")
.RegexReplace("[^a-zA-Z0-9_]", "")
.ToUpper();
if (weaponsObject.ContainsKey(weaponName))
{
_logger.Error($"weapon {weaponName} already exists");
continue;
}
weaponsObject[weaponName] = kv.Key;
}
// Sort the weapons object
var itemList = weaponsObject.ToList();
itemList.Sort((kv1, kv2) => kv1.Key.CompareTo(kv2.Key));
var orderedWeaponsObject = itemList.ToDictionary(kv => kv.Key, kv => kv.Value);
return orderedWeaponsObject;
}
/**
* Clear any non-alpha numeric characters, and fix multiple underscores
* @param enumKey The enum key to sanitize
* @returns The sanitized enum key
*/
private string SanitizeEnumKey(string enumKey)
{
return enumKey
.ToUpper()
.RegexReplace("[^A-Z0-9_]", "")
.RegexReplace("_+", "_");
}
private string GetParentName(TemplateItem item)
{
if (item.Properties?.QuestItem is true)
{
return "QUEST";
}
if (_itemHelper.IsOfBaseclass(item.Id, BaseClasses.BARTER_ITEM))
{
return "BARTER";
}
if (_itemHelper.IsOfBaseclass(item.Id, BaseClasses.THROW_WEAPON))
{
return "GRENADE";
}
if (_itemHelper.IsOfBaseclass(item.Id, BaseClasses.STIMULATOR))
{
return "STIM";
}
if (_itemHelper.IsOfBaseclass(item.Id, BaseClasses.MAGAZINE))
{
return "MAGAZINE";
}
if (_itemHelper.IsOfBaseclass(item.Id, BaseClasses.KEY_MECHANICAL))
{
return "KEY";
}
if (_itemHelper.IsOfBaseclass(item.Id, BaseClasses.MOB_CONTAINER))
{
return "SECURE";
}
if (_itemHelper.IsOfBaseclass(item.Id, BaseClasses.SIMPLE_CONTAINER))
{
return "CONTAINER";
}
if (_itemHelper.IsOfBaseclass(item.Id, BaseClasses.PORTABLE_RANGE_FINDER))
{
return "RANGEFINDER";
}
// Why are flares grenade launcher...?
if (item.Name.StartsWith("weapon_rsp30"))
{
return "FLARE";
}
// This is a special case for the signal pistol, I'm not adding it as a Grenade Launcher
if (item.Id == "620109578d82e67e7911abf2")
{
return "SIGNALPISTOL";
}
var parentId = item.Parent;
return items[parentId].Name.ToUpper();
}
private bool IsValidItem(TemplateItem item)
{
var shrapnelId = "5943d9c186f7745a13413ac9";
if (item.Type != "Item")
{
return false;
}
if (item.Prototype == shrapnelId)
{
return false;
}
return true;
}
/**
* Generate a prefix for the passed in item
* @param item The item to generate the prefix for
* @returns The prefix of the given item
*/
private string GetItemPrefix(TemplateItem item)
{
var prefix = "";
// Prefix ammo with its caliber
if (_itemHelper.IsOfBaseclass(item.Id, BaseClasses.AMMO))
{
prefix = GetAmmoPrefix(item);
}
// Prefix ammo boxes with their caliber
else if (_itemHelper.IsOfBaseclass(item.Id, BaseClasses.AMMO_BOX))
{
prefix = GetAmmoBoxPrefix(item);
}
// Prefix magazines with their caliber
else if (_itemHelper.IsOfBaseclass(item.Id, BaseClasses.MAGAZINE))
{
prefix = GetMagazinePrefix(item);
}
// Make sure there's an underscore separator
if (prefix.Length > 0 && prefix[0] != '_')
{
prefix = $"_{prefix}";
}
return prefix;
}
private string GetItemSuffix(TemplateItem item)
{
var suffix = "";
// Add mag size for magazines
if (_itemHelper.IsOfBaseclass(item.Id, BaseClasses.MAGAZINE))
{
suffix = $"{item.Properties?.Cartridges?[0].MaxCount?.ToString()}RND";
}
// Add pack size for ammo boxes
else if (_itemHelper.IsOfBaseclass(item.Id, BaseClasses.AMMO_BOX))
{
suffix = $"{item.Properties.StackSlots[0]?.MaxCount.ToString()}RND";
}
// Add "DAMAGED" for damaged items
if (item.Name.ToLower().Contains("damaged"))
{
suffix += "_DAMAGED";
}
// Make sure there's an underscore separator
if (suffix.Length > 0 && suffix[0] != '_')
{
suffix = $"_{suffix}";
}
return suffix;
}
private string GetAmmoPrefix(TemplateItem item)
{
var prefix = item.Properties.Caliber.ToUpper();
return CleanCaliber(prefix);
}
private string CleanCaliber(string ammoCaliber)
{
var ammoCaliberToClean = ammoCaliber;
ammoCaliberToClean = ammoCaliberToClean.Replace("CALIBER", "");
ammoCaliberToClean = ammoCaliberToClean.Replace("PARA", "");
ammoCaliberToClean = ammoCaliberToClean.Replace("NATO", "");
// Special case for 45ACP
ammoCaliberToClean = ammoCaliberToClean.Replace("1143X23ACP", "45ACP");
return ammoCaliberToClean;
}
private string GetAmmoBoxPrefix(TemplateItem item)
{
var ammoItem = item.Properties?.StackSlots?[0]?.Props?.Filters?[0]?.Filter?[0];
return GetAmmoPrefix(items[ammoItem]);
}
private string GetMagazinePrefix(TemplateItem item)
{
var ammoItem = item.Properties?.Cartridges?[0]?.Props?.Filters?[0]?.Filter?[0];
return GetAmmoPrefix(items[ammoItem]);
}
/**
* Return the name of the passed in item, formatted for use in an enum
* @param item The item to generate the name for
* @returns The name of the given item
*/
private string GetItemName(TemplateItem item)
{
string? itemName = null;
var localeDb = _localeService.GetLocaleDb();
// Manual item name overrides
if (itemOverrides.ContainsKey(item.Id))
{
itemName = itemOverrides[item.Id].ToUpper();
}
// For the listed types, user the item's _name property
else if (
_itemHelper.IsOfBaseclasses(
item.Id,
[
BaseClasses.RANDOM_LOOT_CONTAINER,
BaseClasses.BUILT_IN_INSERTS,
BaseClasses.STASH
]
)
)
{
itemName = item.Name.ToUpper();
}
// For the listed types, use the short name
else if (_itemHelper.IsOfBaseclasses(item.Id, [BaseClasses.AMMO, BaseClasses.AMMO_BOX, BaseClasses.MAGAZINE]))
{
if (localeDb.TryGetValue($"{item.Id} ShortName", out itemName))
itemName = itemName.ToUpper();
}
// For everything else, use the full name
else
{
if (localeDb.TryGetValue($"{item.Id} Name", out itemName))
itemName = itemName.ToUpper();
}
// Fall back in the event we couldn't find a name
if (string.IsNullOrEmpty(itemName))
{
if (localeDb.TryGetValue($"{item.Id} Name", out itemName))
itemName = itemName.ToUpper();
}
if (string.IsNullOrEmpty(itemName))
{
itemName = item.Name?.ToUpper() ?? null;
}
if (string.IsNullOrEmpty(itemName))
{
Console.WriteLine($"Unable to get shortname for {item.Id}");
return "";
}
itemName = itemName.Trim().RegexReplace("[-.()]", "");
itemName = itemName.RegexReplace("[ ]", "_");
return $"_{itemName}";
}
private string? GetItemNameSuffix(TemplateItem item)
{
var localeDb = _localeService.GetLocaleDb();
localeDb.TryGetValue($"{item.Id} Name", out var itemName);
// Add grid size for lootable containers
if (_itemHelper.IsOfBaseclass(item.Id, BaseClasses.LOOT_CONTAINER))
{
return $"{item.Properties.Grids[0]?.Props.CellsH}X{item.Properties.Grids[0]?.Props.CellsV}";
}
// Add ammo caliber to conflicting weapons
if (_itemHelper.IsOfBaseclass(item.Id, BaseClasses.WEAPON))
{
var caliber = CleanCaliber(item.Properties.AmmoCaliber.ToUpper());
// If the item has a bracketed section at the end of its name, include that
if (itemName?.RegexMatch("\\((.+?)\\)$", out var itemNameBracketSuffix) ?? false)
{
return $"{caliber}_{itemNameBracketSuffix.Groups[1].Value}";
}
return caliber;
}
// Make sure we have a full name
if (string.IsNullOrEmpty(itemName))
{
return "";
}
// If the item has a bracketed section at the end of its name, use that
if (itemName.RegexMatch("\\((.+?)\\)$", out var itemNameBracker))
{
return itemNameBracker.Groups[1].Value;
}
// If the item has a number at the end of its name, use that
if (itemName.RegexMatch("#([0-9]+)$", out var itemNameNumberSuffix))
{
return itemNameNumberSuffix.Groups[1].Value;
}
return "";
}
private void LogEnumValueChanges(Dictionary<string, string> data, string enumName, Type originalEnum)
{
// First generate a mapping of the original enum values to names
var originalEnumValues = new Dictionary<string, string>();
foreach (var field in originalEnum.GetFields())
{
originalEnumValues.Add(field.GetValue(null)!.ToString()!, field.Name);
}
// Loop through our new data, and find any where the given ID's name doesn't match the original enum
foreach (var kv in data)
{
if (originalEnumValues.ContainsKey(kv.Value) && originalEnumValues[kv.Value] != kv.Key)
{
_logger.Warning(
$"Enum {enumName} key has changed for {kv.Value}, {originalEnumValues[kv.Value]} => {kv.Key}"
);
}
}
}
private void WriteEnumsToFile(string outputPath, Dictionary<string, Dictionary<string, string>> enumEntries)
{
var enumFileData =
"// This is an auto generated file, do not modify. Re-generate by running ItemTplGenerator.exe";
foreach (var (enumName, data) in enumEntries)
{
enumFileData += $"\npublic static class {enumName}\n{{\n";
foreach (var (key, value) in data)
{
enumFileData += $" {key} = \"{value}\";\n";
}
enumFileData += "}\n";
}
// TODO: enable once we dont get any more errors
// this.fileSystemSync.write(outputPath, enumFileData);
}
}