diff --git a/Perpetuum.DataDumper/App.config b/Perpetuum.DataDumper/App.config
new file mode 100644
index 000000000..35c2129d0
--- /dev/null
+++ b/Perpetuum.DataDumper/App.config
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ C:\PerpetuumServer\data
+
+
+ C:\PerpetuumServer\data\dictionary.txt
+
+
+
+
diff --git a/Perpetuum.DataDumper/Base Views/ActiveModuleDataView.cs b/Perpetuum.DataDumper/Base Views/ActiveModuleDataView.cs
new file mode 100644
index 000000000..a97757db5
--- /dev/null
+++ b/Perpetuum.DataDumper/Base Views/ActiveModuleDataView.cs
@@ -0,0 +1,14 @@
+namespace Perpetuum.DataDumper {
+ public partial class DataDumper
+ {
+ public class ActiveModuleDataView : ModuleDataView {
+ // These are nullable because some items may be
+ // in a group of active modules but are themselves
+ // passive and we don't want to show 0 for them
+ public double? ModuleAccumulator { get; set; }
+ public double? ModuleCycle { get; set; }
+ public double? ModuleOptimalRange { get; set; }
+ }
+
+ }
+}
diff --git a/Perpetuum.DataDumper/Base Views/EntityDataView.cs b/Perpetuum.DataDumper/Base Views/EntityDataView.cs
new file mode 100644
index 000000000..6fbe440ee
--- /dev/null
+++ b/Perpetuum.DataDumper/Base Views/EntityDataView.cs
@@ -0,0 +1,9 @@
+using System.Collections.Generic;
+
+namespace Perpetuum.DataDumper {
+ public class EntityDataView {
+ public string ItemName { get; set; } // This should actually be renamed...
+ public string ItemKey { get; set; }
+ public List ItemCategories { get; set; }
+ }
+}
diff --git a/Perpetuum.DataDumper/Base Views/ItemDataView.cs b/Perpetuum.DataDumper/Base Views/ItemDataView.cs
new file mode 100644
index 000000000..2566e8600
--- /dev/null
+++ b/Perpetuum.DataDumper/Base Views/ItemDataView.cs
@@ -0,0 +1,12 @@
+namespace Perpetuum.DataDumper {
+ public partial class DataDumper
+ {
+ public class ItemDataView : EntityDataView {
+ public double ItemMass { get; set; }
+ public double ItemVolume { get; set; }
+ public double ItemVolumePacked { get; set; }
+
+ }
+
+ }
+}
diff --git a/Perpetuum.DataDumper/Base Views/ModuleDataView.cs b/Perpetuum.DataDumper/Base Views/ModuleDataView.cs
new file mode 100644
index 000000000..963048171
--- /dev/null
+++ b/Perpetuum.DataDumper/Base Views/ModuleDataView.cs
@@ -0,0 +1,14 @@
+using System.Collections.Generic;
+
+namespace Perpetuum.DataDumper {
+ public partial class DataDumper
+ {
+ public class ModuleDataView : ItemDataView {
+ public string ModuleTier { get; set; }
+ public double ModuleCpu { get; set; }
+ public double ModuleReactor { get; set; }
+ public List ExtensionsRequired { get; set; }
+ }
+
+ }
+}
diff --git a/Perpetuum.DataDumper/DataDumper.cs b/Perpetuum.DataDumper/DataDumper.cs
new file mode 100644
index 000000000..3f61973cf
--- /dev/null
+++ b/Perpetuum.DataDumper/DataDumper.cs
@@ -0,0 +1,381 @@
+using Autofac;
+using Perpetuum.Bootstrapper;
+using Perpetuum.Data;
+using Perpetuum.EntityFramework;
+using Perpetuum.ExportedTypes;
+using Perpetuum.Items;
+using Perpetuum.Modules;
+using Perpetuum.Modules.Weapons;
+using Perpetuum.Robots;
+using Perpetuum.Services.ExtensionService;
+using System;
+using System.Collections.Generic;
+using System.Data;
+using System.Linq;
+using System.Runtime.InteropServices;
+using Newtonsoft.Json;
+using Perpetuum.Items.Ammos;
+using Perpetuum.Services.ProductionEngine;
+using Perpetuum.DataDumper.Views;
+using System.Windows.Forms;
+using System.CodeDom;
+using Perpetuum.Services.Sparks;
+using Perpetuum.Groups.Alliances;
+using System.Reflection;
+using NPOI.SS.UserModel;
+
+namespace Perpetuum.DataDumper {
+ public partial class DataDumper
+ {
+ private string serverRoot;
+ private string dictionaryPath;
+
+ public IExtensionReader ExtensionReader;
+
+ private IContainer container;
+ EntityFactory entityFactory;
+ IEntityServices entityServices;
+ IEntityDefaultReader defaultReader;
+ Dictionary itemNames = new Dictionary();
+ ISparkRepository sparkRepository;
+
+ // Some static lists for helpers
+ public static SlotFlags[] SLOT_SIZE_FLAGS = new SlotFlags[] { SlotFlags.small, SlotFlags.medium, SlotFlags.large };
+ public static SlotFlags[] SLOT_TYPE_FLAGS = new SlotFlags[] { SlotFlags.turret, SlotFlags.missile, SlotFlags.melee, SlotFlags.industrial, SlotFlags.ew_and_engineering };
+ public static SlotFlags[] SLOT_LOCATION_FLAGS = new SlotFlags[] { SlotFlags.head, SlotFlags.chassis, SlotFlags.leg };
+ public static List CATEGORIES_AMMO_WEAPON = new List {
+ CategoryFlags.cf_railgun_ammo,
+ CategoryFlags.cf_laser_ammo,
+ CategoryFlags.cf_missile_ammo,
+ CategoryFlags.cf_projectile_ammo };
+
+ public DataDumper(IContainer container, string serverRoot, string dictionaryPath)
+ {
+ this.serverRoot = serverRoot;
+
+ this.dictionaryPath = dictionaryPath;
+
+ this.container = container;
+
+ entityFactory = container.Resolve();
+
+ ExtensionReader = container.Resolve();
+
+ defaultReader = container.Resolve();
+
+ var productionDataReader = container.Resolve();
+
+ sparkRepository = container.Resolve();
+
+ entityServices = container.Resolve();
+
+ // Testing for new data dumps
+ //
+ // var getEd = defaultReader.GetByName("def_named1_small_armor_repairer");
+ // var getCats = String.Join(";", getEd.CategoryFlags.GetCategoryFlagsTree().Where(x => x.ToString() != "undefined").Select(x => x.ToString()).ToList());
+ // var testMissile = productionDataReader.ProductionComponents[64];
+ // var testRobot = productionDataReader.ProductionComponents[193];
+ // var testRobot2 = productionDataReader.ProductionComponents[208];
+
+ var dataLines = System.IO.File.ReadAllText(dictionaryPath);
+
+ var dictionaryText = GenXY.GenxyConverter.Deserialize(dataLines);
+
+ if (dictionaryText.ContainsKey("dictionary")) {
+ var sourceDict = (Dictionary)dictionaryText["dictionary"];
+
+ foreach (var item in sourceDict) {
+ itemNames.Add(item.Key, item.Value.ToString().Trim());
+ }
+
+ } else {
+ throw new Exception("Dictionary file is invalid");
+ }
+
+ // Now read the names from JSON files
+ string dictionaryLocation = System.IO.Path.Combine(serverRoot, @"customDictionary\0.json");
+
+ var jsonData = JsonConvert.DeserializeObject>(System.IO.File.ReadAllText(dictionaryLocation));
+
+ foreach (var item in jsonData) {
+ if (itemNames.ContainsKey(item.Key)) {
+ itemNames[item.Key] = item.Value;
+ } else {
+ itemNames.Add(item.Key, item.Value);
+ }
+ }
+
+ Console.WriteLine($"{dataLines.Length} dictionary names loaded");
+ }
+
+ public string GetLocalizedName(string itemKey)
+ {
+ if (itemNames.ContainsKey(itemKey))
+ {
+ return itemNames[itemKey];
+ } else
+ {
+ return itemKey;
+ }
+ }
+
+ public static string GenerateCargoDefinition(Type viewType, string tableName, string listDelimiter = ";") {
+ string header = "\n{{#cargo_declare:_table="+tableName+"\n";
+ string body = "";
+ string footer = "}}\n";
+ var allProperties = viewType.GetProperties().OrderBy(x=> x.Name).ToList();
+
+ foreach (var property in allProperties) {
+ var cargoAttribute = property.GetCustomAttribute();
+
+ string cargoType = "String"; // <- This is the default in Cargo
+
+ if (cargoAttribute != null) {
+ cargoType = cargoAttribute.Type;
+ } else {
+ // Let's set some defaults by property type
+ if (property.PropertyType == typeof(double)) {
+ cargoType = "Float";
+ } else if (property.PropertyType == typeof(int)) {
+ cargoType = "Integer";
+ } else if (property.PropertyType == typeof(bool)) {
+ cargoType = "Boolean";
+ } else if(property.PropertyType == typeof(List)) {
+ cargoType = $"List ({listDelimiter}) of String";
+ } else if (property.PropertyType == typeof(List)) {
+ cargoType = $"List ({listDelimiter}) of Integer";
+ } else if (property.PropertyType == typeof(List)) {
+ cargoType = $"List ({listDelimiter}) of Float";
+ }
+ }
+
+ body += $"|{property.Name}={cargoType}\n";
+ }
+
+ return header + body + footer;
+
+ }
+
+ public void InitItemView(ItemDataView view, Entity entity) {
+ view.ItemName = GetLocalizedName(entity.ED.Name);
+ view.ItemKey = entity.ED.Name;
+ view.ItemCategories = entity.ED.CategoryFlags.GetCategoryFlagsTree().Where(x=> x.ToString() != "undefined").Select(x => x.ToString()).ToList();
+ view.ItemMass = entity.Mass;
+ view.ItemVolumePacked = entity.ED.CalculateVolume(true, 1);
+ view.ItemVolume = entity.ED.CalculateVolume(false, 1);
+ }
+
+ public void InitModuleView(ModuleDataView view, Modules.Module module) {
+ InitItemView(view, module);
+
+ view.ModuleTier = module.ED.GameTierString();
+ view.ModuleCpu = module.CpuUsage;
+ view.ModuleReactor = module.PowerGridUsage;
+
+ view.ExtensionsRequired = new List();
+
+ foreach (var extension in module.ED.EnablerExtensions.Keys) {
+ view.ExtensionsRequired.Add(GetLocalizedName(ExtensionReader.GetExtensionName(extension.id)) + "(" + extension.level + ")");
+ }
+ }
+
+ public void InitActiveModuleView(ActiveModuleDataView view, ActiveModule module) {
+ InitModuleView(view, module);
+
+ view.ModuleAccumulator = module.CoreUsage;
+ view.ModuleCycle = module.CycleTime.TotalSeconds;
+
+ var range = module.GetBasePropertyModifier(AggregateField.optimal_range);
+
+ if (range.HasValue) {
+ view.ModuleOptimalRange = range.Value * 10;
+ }
+ }
+
+ public static string GetModifierString(ItemPropertyModifier mod) {
+ var returnValue = "";
+ if (mod.HasValue) {
+ if (mod.ToString().Contains("Formula: Add")) {
+ returnValue = mod.Value * 100 + "%";
+
+ if (mod.Value > 0) {
+ returnValue = "+" + returnValue;
+ }
+ } else {
+ returnValue = ((mod.Value - 1) * 100) + "%";
+
+ if (mod.Value - 1 > 0) {
+ returnValue = "+" + returnValue;
+ }
+ }
+
+ }
+
+ return returnValue;
+ }
+
+ public void WriteDataView(List> dataRows, string sheetName, ISheet worksheet, ref int currentDataRow) {
+ // Deal with the header
+ // If we are writing later than the first row in our sheet then we will skip
+ // the first row in our data because it will contain the header
+ int skipRow = 0;
+
+ if (currentDataRow > 0) {
+ skipRow = 1;
+ }
+
+ foreach (var dataRow in dataRows.Skip(skipRow).ToList()) {
+ int currentColumn = 0;
+ var currentRow = worksheet.CreateRow(currentDataRow);
+ foreach (var dataValue in dataRow) {
+ currentRow.CreateCell(currentColumn).SetCellValue(dataValue);
+ currentColumn++;
+ }
+ currentDataRow++;
+ }
+ }
+
+ ///
+ /// This will write the properties as headers and values as rows
+ ///
+ public List> ComposeDataView(List data, string wikiTableName = "wikitable") {
+ var returnData = new List>();
+
+ if (data is null || data.Count == 0) {
+ return returnData; // Do nothing
+ }
+
+ var headers = data.First().GetType().GetProperties().Select(i => i.Name).ToList();
+
+ returnData.Add(headers.Concat(new List { "wiki" }).ToList());
+
+ foreach (var item in data)
+ {
+ var currentProperties = new List();
+
+ // Add the wiki markup at the end
+ // {{#cargo_store:_table=WeaponStats|module_name=Niani medium EM-gun|module_key=def_artifact_a_longrange_medium_railgun|module_categories=cf_medium_railguns;cf_railguns;cf_weapons;cf_robot_equipment|cpu=45|reactor=205|ammo_type=Medium slugs|slot_status=Active|ammo_capacity=50|module_mass=650|module_volume_packed=0.25|module_tier=T3+|module_volume=0.5|module_accumulator=32|module_cycle=10|module_damage=250|module_falloff=60|module_hit_dispersion=14|module_optimal_range=300|module_extensions_required=Advanced magnetostatics(2);|slot_type=turret|slot_size=medium|slot_location=chassis}}
+ string wikiData = $"{{{{#cargo_store:_table={wikiTableName}";
+
+ foreach (string prop in headers)
+ {
+ var currentProp = item.GetType().GetProperty(prop);
+ var currentValue = "";
+
+ try
+ {
+ if (currentProp == null)
+ {
+ currentValue = "Error: Prop not found";
+ }
+ else
+ {
+ if (currentProp.PropertyType.Name.Contains("List")) {
+ currentValue = String.Join(";", (currentProp.GetValue(item) as IEnumerable