Added ability to Add / Del new root nodes for some things. Added loading of items from a Json file to easily select them while editing.

This commit is contained in:
Matthew Neumann 2025-03-05 16:04:27 -08:00
parent 1a0cc36907
commit 0691e2c99e
11 changed files with 2563 additions and 87 deletions

View File

@ -9,4 +9,25 @@
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<None Remove="beltitems.json" />
<None Remove="weaponmoditems.json" />
<None Remove="wearitems.json" />
</ItemGroup>
<ItemGroup>
<Content Include="allitems.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="beltitems.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="weaponmoditems.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="wearitems.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>

View File

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;
@ -9,12 +10,15 @@ namespace Better_NCP_Editor
public object NewValue { get; private set; }
private Control inputControl;
public EditValueForm(string propertyName, string currentValue, Type valueType)
// Constructor now takes an extra parameter: parentNodeName.
public EditValueForm(string propertyName, string currentValue, Type valueType, List<string>? comboItems)
{
// Set the minimum size.
this.MinimumSize = new Size(210, 150); // Example: 300x150 pixels
// Optionally, set the default size if you want:
this.ClientSize = new Size(210, 150);
// Set the minimum and default size.
this.MinimumSize = new Size(210, 120);
this.FormBorderStyle = FormBorderStyle.FixedDialog;
this.MaximizeBox = false;
// Measure the width of the property name.
Size keySize = TextRenderer.MeasureText(propertyName, this.Font);
@ -27,14 +31,13 @@ namespace Better_NCP_Editor
}
else
{
inputWidth = TextRenderer.MeasureText(currentValue, this.Font).Width + 20;
inputWidth = TextRenderer.MeasureText(currentValue, this.Font).Width + 40;
}
// Determine the desired width: the maximum of the key width and input width, plus extra padding.
int desiredWidth = Math.Max(keySize.Width, inputWidth) + 40;
// Set the form's client size (you can also set MinimumSize here if needed).
this.ClientSize = new Size(desiredWidth, 120);
this.Text = $"Edit {propertyName}";
this.StartPosition = FormStartPosition.CenterParent;
@ -47,32 +50,79 @@ namespace Better_NCP_Editor
};
this.Controls.Add(lblProperty);
// Create the input control with width based on desiredWidth minus margins.
int controlWidth = desiredWidth - 20;
if (valueType == typeof(bool))
// Check if we need to use a special ComboBox (for "ShortName" in "Wear items").
if (comboItems != null)
{
ComboBox combo = new ComboBox()
{
Location = new Point(10, 40),
Width = controlWidth,
Width = controlWidth, // initial width based on desiredWidth minus margins
DropDownStyle = ComboBoxStyle.DropDownList
};
combo.Items.Add("true");
combo.Items.Add("false");
combo.SelectedItem = currentValue;
int maxWidth = 0;
foreach (var key in comboItems)
{
combo.Items.Add(key);
// Measure each item's width using the ComboBox's font.
Size itemSize = TextRenderer.MeasureText(key, combo.Font);
if (itemSize.Width > maxWidth)
maxWidth = itemSize.Width;
}
// Optionally add some extra padding.
int paddedWidth = maxWidth + 20;
// Set the DropDownWidth to ensure items are fully visible.
combo.DropDownWidth = paddedWidth;
// Optionally, you can also adjust the combo's Width if you want it to match the dropdown width.
if (combo.Width < paddedWidth)
combo.Width = paddedWidth;
// Set selected item to currentValue if found, otherwise select the first item.
if (comboItems.Contains(currentValue))
{
combo.SelectedItem = currentValue;
}
else if (combo.Items.Count > 0)
{
combo.SelectedIndex = 0;
}
inputControl = combo;
this.Controls.Add(combo);
}
else
{
TextBox txtBox = new TextBox()
// Create the input control normally.
if (valueType == typeof(bool))
{
Location = new Point(10, 40),
Width = controlWidth,
Text = currentValue
};
inputControl = txtBox;
this.Controls.Add(txtBox);
ComboBox combo = new ComboBox()
{
Location = new Point(10, 40),
Width = controlWidth,
DropDownStyle = ComboBoxStyle.DropDownList
};
combo.Items.Add("true");
combo.Items.Add("false");
combo.SelectedItem = currentValue;
inputControl = combo;
this.Controls.Add(combo);
}
else
{
TextBox txtBox = new TextBox()
{
Location = new Point(10, 40),
Width = controlWidth,
Text = currentValue
};
inputControl = txtBox;
this.Controls.Add(txtBox);
}
}
// OK button.
@ -91,24 +141,31 @@ namespace Better_NCP_Editor
Button btnCancel = new Button()
{
Text = "Cancel",
Location = new Point(100, 80), // Positioned to the right of OK.
Location = new Point(100, 80),
Size = new Size(80, 25),
DialogResult = DialogResult.Cancel,
Anchor = AnchorStyles.Left | AnchorStyles.Bottom
};
this.Controls.Add(btnCancel);
this.AcceptButton = btnOk;
this.CancelButton = btnCancel;
}
private void BtnOk_Click(object sender, EventArgs e)
{
// Determine new value based on input control.
if (inputControl is ComboBox combo)
// If the input control is the special ComboBox, return the dictionary value.
if (inputControl is ComboBox combo && combo.DropDownStyle == ComboBoxStyle.DropDownList &&
combo.Items.Count > 0)
{
NewValue = combo.SelectedItem.ToString() == "true";
NewValue = combo.SelectedItem.ToString();
}
else if (inputControl is ComboBox comboBool)
{
NewValue = comboBool.SelectedItem.ToString() == "true";
}
else if (inputControl is TextBox txt)
{
// Try to parse an integer; if it fails, treat it as a string.
if (int.TryParse(txt.Text, out int intValue))
NewValue = intValue;
else

View File

@ -43,9 +43,10 @@
//
// btn_load
//
btn_load.Location = new Point(17, 12);
btn_load.Location = new Point(15, 9);
btn_load.Margin = new Padding(3, 2, 3, 2);
btn_load.Name = "btn_load";
btn_load.Size = new Size(135, 29);
btn_load.Size = new Size(118, 22);
btn_load.TabIndex = 0;
btn_load.Text = "Load Directory";
btn_load.UseVisualStyleBackColor = true;
@ -54,9 +55,10 @@
// btn_save
//
btn_save.Enabled = false;
btn_save.Location = new Point(214, 12);
btn_save.Location = new Point(187, 9);
btn_save.Margin = new Padding(3, 2, 3, 2);
btn_save.Name = "btn_save";
btn_save.Size = new Size(93, 29);
btn_save.Size = new Size(81, 22);
btn_save.TabIndex = 1;
btn_save.Text = "Save File";
btn_save.UseVisualStyleBackColor = true;
@ -64,24 +66,27 @@
//
// dirTreeView
//
dirTreeView.Location = new Point(17, 53);
dirTreeView.Location = new Point(15, 40);
dirTreeView.Margin = new Padding(3, 2, 3, 2);
dirTreeView.Name = "dirTreeView";
dirTreeView.Size = new Size(290, 812);
dirTreeView.Size = new Size(254, 610);
dirTreeView.TabIndex = 2;
//
// entityTreeView
//
entityTreeView.Location = new Point(313, 53);
entityTreeView.Location = new Point(274, 40);
entityTreeView.Margin = new Padding(3, 2, 3, 2);
entityTreeView.Name = "entityTreeView";
entityTreeView.Size = new Size(843, 812);
entityTreeView.Size = new Size(738, 610);
entityTreeView.TabIndex = 3;
//
// btn_entity_del
//
btn_entity_del.Enabled = false;
btn_entity_del.Location = new Point(366, 12);
btn_entity_del.Location = new Point(320, 9);
btn_entity_del.Margin = new Padding(3, 2, 3, 2);
btn_entity_del.Name = "btn_entity_del";
btn_entity_del.Size = new Size(47, 29);
btn_entity_del.Size = new Size(41, 22);
btn_entity_del.TabIndex = 8;
btn_entity_del.Text = "Del";
btn_entity_del.UseVisualStyleBackColor = true;
@ -90,9 +95,10 @@
// btn_entity_add
//
btn_entity_add.Enabled = false;
btn_entity_add.Location = new Point(313, 12);
btn_entity_add.Location = new Point(274, 9);
btn_entity_add.Margin = new Padding(3, 2, 3, 2);
btn_entity_add.Name = "btn_entity_add";
btn_entity_add.Size = new Size(46, 29);
btn_entity_add.Size = new Size(40, 22);
btn_entity_add.TabIndex = 7;
btn_entity_add.Text = "Add";
btn_entity_add.UseVisualStyleBackColor = true;
@ -101,9 +107,10 @@
// btn_import_entityData
//
btn_import_entityData.Enabled = false;
btn_import_entityData.Location = new Point(493, 12);
btn_import_entityData.Location = new Point(431, 9);
btn_import_entityData.Margin = new Padding(3, 2, 3, 2);
btn_import_entityData.Name = "btn_import_entityData";
btn_import_entityData.Size = new Size(70, 29);
btn_import_entityData.Size = new Size(61, 22);
btn_import_entityData.TabIndex = 9;
btn_import_entityData.Text = "Import";
btn_import_entityData.UseVisualStyleBackColor = true;
@ -112,9 +119,10 @@
// btn_export_entityData
//
btn_export_entityData.Enabled = false;
btn_export_entityData.Location = new Point(569, 12);
btn_export_entityData.Location = new Point(498, 9);
btn_export_entityData.Margin = new Padding(3, 2, 3, 2);
btn_export_entityData.Name = "btn_export_entityData";
btn_export_entityData.Size = new Size(66, 29);
btn_export_entityData.Size = new Size(58, 22);
btn_export_entityData.TabIndex = 10;
btn_export_entityData.Text = "Export";
btn_export_entityData.UseVisualStyleBackColor = true;
@ -124,17 +132,18 @@
//
statusTextbox.BorderStyle = BorderStyle.FixedSingle;
statusTextbox.ImeMode = ImeMode.NoControl;
statusTextbox.Location = new Point(660, 14);
statusTextbox.Location = new Point(578, 10);
statusTextbox.Margin = new Padding(3, 2, 3, 2);
statusTextbox.Name = "statusTextbox";
statusTextbox.ReadOnly = true;
statusTextbox.Size = new Size(496, 27);
statusTextbox.Size = new Size(434, 23);
statusTextbox.TabIndex = 11;
//
// Form1
//
AutoScaleDimensions = new SizeF(8F, 20F);
AutoScaleDimensions = new SizeF(7F, 15F);
AutoScaleMode = AutoScaleMode.Font;
ClientSize = new Size(1176, 881);
ClientSize = new Size(1028, 661);
Controls.Add(statusTextbox);
Controls.Add(btn_export_entityData);
Controls.Add(btn_import_entityData);
@ -144,6 +153,7 @@
Controls.Add(dirTreeView);
Controls.Add(btn_save);
Controls.Add(btn_load);
Margin = new Padding(3, 2, 3, 2);
Name = "Form1";
Text = "BetterNPC Editor V1.0";
Load += Form1_Load;

View File

@ -18,6 +18,10 @@ namespace Better_NCP_Editor
// In your Form1 class:
private ToolTip customToolTip = new ToolTip();
private TreeNode lastHoveredNode = null;
private List<String> _wearItems;
private List<String> _beltItems;
private List<String> _weaponModItems;
private Dictionary<String, String> _allItems;
public Form1()
{
@ -27,7 +31,7 @@ namespace Better_NCP_Editor
entityTreeView.AfterSelect += entityTreeView_AfterSelect;
// Prevent the form from shrinking below its current size.
this.MinimumSize = new Size(1194, 928); // Set minimum allowed size
this.MinimumSize = new Size(1044, 700); // Set minimum allowed size
this.FormBorderStyle = FormBorderStyle.Sizable;
// Example layout using docking:
@ -54,8 +58,41 @@ namespace Better_NCP_Editor
entityTreeView.MouseMove += EntityTreeView_MouseMove;
entityTreeView.MouseLeave += EntityTreeView_MouseLeave;
// Load the item database.
_wearItems = LoadItemList("wearitems.json");
_allItems = LoadItemDatabase("allitems.json");
_beltItems = LoadItemList("beltitems.json");
_weaponModItems = LoadItemList("weaponmoditems.json");
}
private List<String> LoadItemList(String filepath)
{
try
{
return JsonListLoader.Load(filepath);
}
catch (Exception ex)
{
Console.WriteLine("Error loading JSON file: " + ex.Message);
return null;
}
}
private Dictionary<String,String> LoadItemDatabase(String filepath)
{
try
{
return JsonDictionaryLoader.Load(filepath);
}
catch (Exception ex)
{
Console.WriteLine("Error loading JSON file: " + ex.Message);
return null;
}
}
private void EntityTreeView_MouseMove(object sender, MouseEventArgs e)
{
int xOffset = 15;
@ -252,7 +289,7 @@ namespace Better_NCP_Editor
// Enable if the selected node's Tag is a JsonArray
if (e.Node.Tag is JsonArray)
{
//enableButtons = true;
enableAddDelButtons = true;
enableImportExportButtons = true;
}
// Or if the selected node is an item in an array (its parent is a JsonArray)
@ -261,6 +298,11 @@ namespace Better_NCP_Editor
enableAddDelButtons = true;
enableImportExportButtons = true;
}
if (e.Node.Text.Equals("Presets", StringComparison.OrdinalIgnoreCase))
{
enableAddDelButtons = false;
}
// You can also add additional conditions for a "preset" node, e.g. by checking text:
// else if (e.Node.Text.StartsWith("Preset", StringComparison.OrdinalIgnoreCase))
// {
@ -301,28 +343,68 @@ namespace Better_NCP_Editor
valueType = typeof(bool);
else if (int.TryParse(currentVal, out int i))
valueType = typeof(int);
using (EditValueForm editForm = new EditValueForm(propName, currentVal, valueType))
String parentNodeName = "Root";
String grandParentNodeName = "Root";
List<String> comboList = null;
if (e.Node.Parent != null)
{
if (editForm.ShowDialog() == DialogResult.OK)
{
object newVal = editForm.NewValue;
string displayVal = newVal is bool ? newVal.ToString().ToLower() : newVal.ToString();
e.Node.Text = $"{propName}: {displayVal}";
parentNodeName = e.Node.Parent.Text;
if (e.Node.Tag is JsonNode node)
if (e.Node.Parent.Parent != null)
{
grandParentNodeName = e.Node.Parent.Parent.Text;
if (propName.Equals("ShortName", StringComparison.OrdinalIgnoreCase) && grandParentNodeName.Equals("Wear items"))
{
JsonNode newNode = JsonValue.Create(newVal);
node.ReplaceWith(newNode);
e.Node.Tag = newNode;
comboList = _wearItems;
}
// Mark the file as modified.
fileModified = true;
btn_save.Enabled = true;
else if (propName.Equals("ShortName", StringComparison.OrdinalIgnoreCase) && grandParentNodeName.Equals("Belt items"))
{
comboList = _beltItems;
}
}
if (parentNodeName.Equals("Mods"))
{
comboList = _weaponModItems;
}
}
using (EditValueForm editForm = new EditValueForm(propName, currentVal, valueType, comboList))
{
if (editForm.ShowDialog() == DialogResult.OK)
{
object newVal = editForm.NewValue;
string displayVal = newVal is bool ? newVal.ToString().ToLower() : newVal.ToString();
if (_allItems.ContainsKey(displayVal))
{
newVal = _allItems[displayVal];
displayVal = _allItems[displayVal];
}
e.Node.Text = $"{propName}: {displayVal}";
if (e.Node.Tag is JsonNode node)
{
JsonNode newNode = JsonValue.Create(newVal);
node.ReplaceWith(newNode);
e.Node.Tag = newNode;
}
// Mark the file as modified.
fileModified = true;
btn_save.Enabled = true;
}
}
}
@ -378,55 +460,124 @@ namespace Better_NCP_Editor
TreeNode selected = entityTreeView.SelectedNode;
if (selected == null)
{
MessageBox.Show("Please select a node to duplicate.", "Info", MessageBoxButtons.OK, MessageBoxIcon.Information);
MessageBox.Show("Please select a node to add a new item.", "Info", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
TreeNode parent = selected.Parent;
if (parent == null)
JsonArray targetArray = null;
bool duplicateExisting = false;
// If the selected node itself is an array node...
if (selected.Tag is JsonArray arr)
{
MessageBox.Show("The selected node has no parent (cannot duplicate).", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
targetArray = arr;
duplicateExisting = false; // We'll add a new blank node
}
// Ensure the parent's Tag is a JsonArray.
if (!(parent.Tag is JsonArray parentArray))
// Otherwise, if the selected node's parent is an array...
else if (selected.Parent != null && selected.Parent.Tag is JsonArray arrParent)
{
MessageBox.Show("The selected node's parent is not an array node.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
targetArray = arrParent;
duplicateExisting = true; // We duplicate the selected item
}
// Retrieve the underlying JsonNode for the selected node.
if (!(selected.Tag is JsonNode selectedJsonNode))
else
{
MessageBox.Show("Selected node does not contain a valid JSON element.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
MessageBox.Show("The selected node is not part of an array.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
// Duplicate (deep clone) the selected JsonNode.
JsonNode duplicate = selectedJsonNode.DeepClone();
JsonNode newNode;
if (!duplicateExisting)
{
// We're adding a new blank node to the array.
// Use the selected node's text as the array name.
string arrayName = selected.Text.Trim();
if (arrayName.Equals("Wear items", StringComparison.OrdinalIgnoreCase))
{
var obj = new JsonObject();
obj["ShortName"] = "CHANGE ME";
obj["SkinID (0 - default)"] = 0;
newNode = obj;
}
else if (arrayName.Equals("Belt items", StringComparison.OrdinalIgnoreCase))
{
var obj = new JsonObject();
obj["ShortName"] = "CHANGE ME";
obj["Amount"] = 1;
obj["SkinID (0 - default)"] = 0;
obj["Mods"] = new JsonArray(); // Empty array
obj["Ammo"] = "";
newNode = obj;
} else if (arrayName.Equals("List of prefabs", StringComparison.OrdinalIgnoreCase))
{
var obj = new JsonObject();
obj["Chance [0.0-100.0]"] = 100.0;
obj["The path to the prefab"] = "assets/rust.ai/agents/npcplayer/humannpc/scientist/CHANGEME";
newNode = obj;
}
else if (arrayName.Equals("List of items", StringComparison.OrdinalIgnoreCase))
{
var obj = new JsonObject();
obj["ShortName"] = "CHANGEME";
obj["Minimum"] = 1;
obj["Maximum"] = 100;
obj["Chance [0.0-100.0]"] = 50.0;
obj["Is this a blueprint? [true/false]"] = false;
obj["SkinID (0 - default)"] = 0;
obj["Name (empty - default)"] = "";
newNode = obj;
}
else if (arrayName.Equals("Mods", StringComparison.OrdinalIgnoreCase))
{
newNode = JsonValue.Create("CHANGE ME");
}
else if (arrayName.Equals("Names", StringComparison.OrdinalIgnoreCase))
{
newNode = JsonValue.Create("CHANGE ME");
}
else
{
// Default: create an empty string
newNode = JsonValue.Create("CHANGE ME");
}
}
else
{
// Duplicate the selected item.
if (!(selected.Tag is JsonNode selectedJsonNode))
{
MessageBox.Show("Selected node does not contain a valid JSON element.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
newNode = selectedJsonNode.DeepClone();
}
// Add the duplicate to the parent's array.
parentArray.Add(duplicate);
// Add the new node to the target array.
targetArray.Add(newNode);
// Refresh only the parent's children.
parent.Nodes.Clear();
PopulateTreeRecursive((JsonNode)parent.Tag, parent);
parent.ExpandAll(); // Expand the parent and all its child nodes
// Determine the node representing the array.
// If duplicating, the array node is the selected node's parent;
// otherwise it is the selected node.
TreeNode arrayNode = duplicateExisting ? selected.Parent : selected;
// Refresh only the children of the array node.
arrayNode.Nodes.Clear();
PopulateTreeRecursive((JsonNode)arrayNode.Tag, arrayNode);
arrayNode.ExpandAll();
fileModified = true;
btn_save.Enabled = true;
// Select the newly added item (last child in the parent's node collection).
if (parent.Nodes.Count > 0)
// Select the newly added item (the last child).
if (arrayNode.Nodes.Count > 0)
{
TreeNode newSelected = parent.Nodes[parent.Nodes.Count - 1];
TreeNode newSelected = arrayNode.Nodes[arrayNode.Nodes.Count - 1];
entityTreeView.SelectedNode = newSelected;
newSelected.EnsureVisible();
entityTreeView.Focus();
}
}
private void btn_entity_del_Click(object sender, EventArgs e)
{
TreeNode selected = entityTreeView.SelectedNode;

View File

@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
namespace Better_NCP_Editor
{
public static class JsonDictionaryLoader
{
public static Dictionary<string, string> Load(string filePath)
{
if (!File.Exists(filePath))
throw new FileNotFoundException("The specified JSON file was not found.", filePath);
string json = File.ReadAllText(filePath);
Dictionary<string, string> dictionary = JsonSerializer.Deserialize<Dictionary<string, string>>(json);
return dictionary;
}
}
}

View File

@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
namespace Better_NCP_Editor
{
public static class JsonListLoader
{
public static List<string> Load(string filePath)
{
if (!File.Exists(filePath))
throw new FileNotFoundException("The specified JSON file was not found.", filePath);
string json = File.ReadAllText(filePath);
List<string> list = JsonSerializer.Deserialize<List<string>>(json);
return list;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,3 @@
[
"MP5A4"
]

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,3 @@
[
"Holosight"
]

View File

@ -0,0 +1,113 @@
[
"Abyss Divers Suit",
"Arctic Scientist Suit",
"Barrel Costume",
"Bandana Mask",
"Baseball Cap",
"Beenie Hat",
"Blue Jumpsuit",
"Bone Armor",
"Bone Helmet",
"Boonie Hat",
"Boots",
"Bucket Helmet",
"Bunny Hat",
"Bunny Onesie",
"Burlap Gloves",
"Burlap Headwrap",
"Burlap Shirt",
"Burlap Shoes",
"Burlap Trousers",
"Candle Hat",
"Chicken Costume",
"Clatter Helmet",
"Coffee Can Helmet",
"Crate Costume",
"Diving Fins",
"Diving Mask",
"Diving Tank",
"Dracula Cape",
"Dracula Mask",
"Dragon Mask",
"Egg Suit",
"Frankenstein Mask",
"Frog Boots",
"Frontier Suit",
"Gas Mask",
"Ghost Costume",
"Gingerbread Suit",
"Hazmat Suit",
"Heavy Plate Helmet",
"Heavy Plate Jacket",
"Heavy Plate Pants",
"Heavy Scientist Suit",
"Hide Boots",
"Hide Halterneck",
"Hide Pants",
"Hide Poncho",
"Hide Skirt",
"Hide Vest",
"Hockey Mask",
"Ice Metal Chest Plate",
"Ice Metal Facemask",
"Improvised Balaclava",
"Jacket",
"Jumpsuit",
"Knights armour cuirass",
"Knights armour helmet",
"Knights armour skirt plates",
"Leather Gloves",
"Lumberjack Hoodie",
"Lumberjack Suit",
"Metal Chest Plate",
"Metal Facemask",
"Miner Hat",
"Mummy Mask",
"Mummy Suit",
"Nest Hat",
"Night Vision Goggles",
"Ninja Suit",
"Nomad Suit",
"NVGM Scientist Suit",
"Ox Mask",
"Pants",
"Party Hat",
"Prisoner Hood",
"Purple Sunglasses",
"Rabbit Mask",
"Rat Mask",
"Reindeer Antlers",
"Riot Helmet",
"Roadsign Gloves",
"Road Sign Jacket",
"Road Sign Kilt",
"Santa Beard",
"Santa Hat",
"Scarecrow Suit",
"Scarecrow Wrap",
"Scientist Suit",
"Shirt",
"Shorts",
"Snake mask",
"Snow Jacket",
"Space Suit",
"Sunglasses",
"Sunglasses Black",
"Sunglasses Camo",
"Sunglasses Red",
"Sunglasses Black2",
"Sunglasses Chrome",
"Sunglasses Gold",
"Surgeon Scrubs",
"Tactical Gloves",
"Tank Top",
"Tiger Mask",
"T-Shirt",
"Twitch Rivals Hazmat Suit",
"Waterwell NPC Jumpsuit",
"Wetsuit",
"Wolf Headdress",
"Wood Armor Helmet",
"Wood Chestplate",
"Wooden Armor Pants"
]