From 34e44def21a562ad2af9556f17b667fbe760aed2 Mon Sep 17 00:00:00 2001 From: Matt Neumann Date: Tue, 4 Mar 2025 20:20:08 -0800 Subject: [PATCH] First commit. --- .gitignore | 400 ++++++++++++ Better NCP Editor.sln | 25 + Better NCP Editor/Better NCP Editor.csproj | 12 + Better NCP Editor/BetterNPCEntity.cs | 354 ++++++++++ Better NCP Editor/EditValueForm.cs | 97 +++ Better NCP Editor/EditValueForm.resx | 120 ++++ Better NCP Editor/Form1.Designer.cs | 166 +++++ Better NCP Editor/Form1.cs | 716 +++++++++++++++++++++ Better NCP Editor/Form1.resx | 120 ++++ Better NCP Editor/Program.cs | 17 + Better NCP Editor/ToolTips.cs | 78 +++ README.md | 2 + 12 files changed, 2107 insertions(+) create mode 100644 .gitignore create mode 100644 Better NCP Editor.sln create mode 100644 Better NCP Editor/Better NCP Editor.csproj create mode 100644 Better NCP Editor/BetterNPCEntity.cs create mode 100644 Better NCP Editor/EditValueForm.cs create mode 100644 Better NCP Editor/EditValueForm.resx create mode 100644 Better NCP Editor/Form1.Designer.cs create mode 100644 Better NCP Editor/Form1.cs create mode 100644 Better NCP Editor/Form1.resx create mode 100644 Better NCP Editor/Program.cs create mode 100644 Better NCP Editor/ToolTips.cs create mode 100644 README.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ca1c7a3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,400 @@ +# ---> VisualStudio +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml + diff --git a/Better NCP Editor.sln b/Better NCP Editor.sln new file mode 100644 index 0000000..1d6e8ca --- /dev/null +++ b/Better NCP Editor.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.13.35806.99 d17.13 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Better NCP Editor", "Better NCP Editor\Better NCP Editor.csproj", "{F8476FF1-BC01-4E29-8173-BAE156F04A7D}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F8476FF1-BC01-4E29-8173-BAE156F04A7D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F8476FF1-BC01-4E29-8173-BAE156F04A7D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F8476FF1-BC01-4E29-8173-BAE156F04A7D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F8476FF1-BC01-4E29-8173-BAE156F04A7D}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {1B1849C0-03C6-4E31-90CB-59B8DDE61F4D} + EndGlobalSection +EndGlobal diff --git a/Better NCP Editor/Better NCP Editor.csproj b/Better NCP Editor/Better NCP Editor.csproj new file mode 100644 index 0000000..5b8389b --- /dev/null +++ b/Better NCP Editor/Better NCP Editor.csproj @@ -0,0 +1,12 @@ + + + + WinExe + net8.0-windows + Better_NCP_Editor + enable + true + enable + + + \ No newline at end of file diff --git a/Better NCP Editor/BetterNPCEntity.cs b/Better NCP Editor/BetterNPCEntity.cs new file mode 100644 index 0000000..9b2b526 --- /dev/null +++ b/Better NCP Editor/BetterNPCEntity.cs @@ -0,0 +1,354 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Better_NCP_Editor +{ + public class BetterNPCEntity + { + // In BetterNPCEntity class + public BetterNPCEntity() + { + Enabled = false; + MonumentSize = ""; + RemoveOtherNPCs = false; + Presets = new List(); + } + + [JsonPropertyName("Enabled? [true/false]")] + public bool Enabled { get; set; } + + // Marked as nullable since some JSON files may not include this key. + [JsonPropertyName("The size of the monument")] + public string? MonumentSize { get; set; } + + // Marked as nullable so a missing key won’t cause issues. + [JsonPropertyName("Remove other NPCs? [true/false]")] + public bool? RemoveOtherNPCs { get; set; } + + [JsonPropertyName("Presets")] + public List Presets { get; set; } + } + + public class Preset + { + // In Preset class + public Preset() + { + Enabled = true; + MinimumNumbersDay = 1; + MaximumNumbersDay = 1; + MinimumNumbersNight = 1; + MaximumNumbersNight = 1; + NPCSetting = new NPCSetting(); + Economics = new EconomicsInfo(); + AppearanceType = 0; + OwnLocations = new List(); + ReturnToAppearance = false; + NavigationGridType = 0; + CratePath = ""; + LootTableType = 0; + PrefabLootTable = new PrefabLootTable(); + OwnLootTable = new OwnLootTable(); + } + + [JsonPropertyName("Enabled? [true/false]")] + public bool Enabled { get; set; } + + [JsonPropertyName("Minimum numbers - Day")] + public int MinimumNumbersDay { get; set; } + + [JsonPropertyName("Maximum numbers - Day")] + public int MaximumNumbersDay { get; set; } + + [JsonPropertyName("Minimum numbers - Night")] + public int MinimumNumbersNight { get; set; } + + [JsonPropertyName("Maximum numbers - Night")] + public int MaximumNumbersNight { get; set; } + + [JsonPropertyName("NPCs setting")] + public NPCSetting NPCSetting { get; set; } + + [JsonPropertyName("The amount of economics that is given for killing the NPC")] + public EconomicsInfo Economics { get; set; } + + [JsonPropertyName("Type of appearance (0 - random; 1 - own list) (not used for Road and Biome)")] + public int AppearanceType { get; set; } + + [JsonPropertyName("Own list of locations (not used for Road and Biome)")] + public List OwnLocations { get; set; } + + [JsonPropertyName("If the NPC ends up below ocean sea level, should the NPC return to it's place of appearance? [true/false]")] + public bool ReturnToAppearance { get; set; } + + [JsonPropertyName("Type of navigation grid (0 - used mainly on the island, 1 - used mainly under water or under land, as well as outside the map, can be used on some monuments)")] + public int NavigationGridType { get; set; } + + [JsonPropertyName("The path to the crate that appears at the place of death (empty - not used)")] + public string CratePath { get; set; } + + [JsonPropertyName("Which loot table should the plugin use? (0 - default; 1 - own; 2 - AlphaLoot; 3 - CustomLoot; 4 - loot table of the Rust objects; 5 - combine the 1 and 4 methods)")] + public int LootTableType { get; set; } + + [JsonPropertyName("Loot table from prefabs (if the loot table type is 4 or 5)")] + public PrefabLootTable PrefabLootTable { get; set; } + + [JsonPropertyName("Own loot table (if the loot table type is 1 or 5)")] + public OwnLootTable OwnLootTable { get; set; } + } + + public class NPCSetting + { + // In NPCSetting class + public NPCSetting() + { + Names = new List(); + Health = 100.0; + RoamRange = 0.0; + ChaseRange = 0.0; + AttackRangeMultiplier = 1.0; + SenseRange = 0.0; + TargetMemoryDuration = 0.0; + ScaleDamage = 1.0; + AimConeScale = 1.0; + DetectInVisionCone = false; + VisionCone = 0.0; + Speed = 0.0; + MinAppearanceTime = 0.0; + MaxAppearanceTime = 0.0; + DisableRadioEffects = false; + IsStationary = false; + RemoveCorpse = false; + WearItems = new List(); + BeltItems = new List(); + Kits = new List(); + } + + [JsonPropertyName("Names")] + public List Names { get; set; } + + [JsonPropertyName("Health")] + public double Health { get; set; } + + [JsonPropertyName("Roam Range")] + public double RoamRange { get; set; } + + [JsonPropertyName("Chase Range")] + public double ChaseRange { get; set; } + + [JsonPropertyName("Attack Range Multiplier")] + public double AttackRangeMultiplier { get; set; } + + [JsonPropertyName("Sense Range")] + public double SenseRange { get; set; } + + [JsonPropertyName("Target Memory Duration [sec.]")] + public double TargetMemoryDuration { get; set; } + + [JsonPropertyName("Scale damage")] + public double ScaleDamage { get; set; } + + [JsonPropertyName("Aim Cone Scale")] + public double AimConeScale { get; set; } + + [JsonPropertyName("Detect the target only in the NPC's viewing vision cone? [true/false]")] + public bool DetectInVisionCone { get; set; } + + [JsonPropertyName("Vision Cone")] + public double VisionCone { get; set; } + + [JsonPropertyName("Speed")] + public double Speed { get; set; } + + [JsonPropertyName("Minimum time of appearance after death (not used for Events) [sec.]")] + public double MinAppearanceTime { get; set; } + + [JsonPropertyName("Maximum time of appearance after death (not used for Events) [sec.]")] + public double MaxAppearanceTime { get; set; } + + [JsonPropertyName("Disable radio effects? [true/false]")] + public bool DisableRadioEffects { get; set; } + + [JsonPropertyName("Is this a stationary NPC? [true/false]")] + public bool IsStationary { get; set; } + + [JsonPropertyName("Remove a corpse after death? (it is recommended to use the true value to improve performance) [true/false]")] + public bool RemoveCorpse { get; set; } + + [JsonPropertyName("Wear items")] + public List WearItems { get; set; } + + [JsonPropertyName("Belt items")] + public List BeltItems { get; set; } + + [JsonPropertyName("Kits (it is recommended to use the previous 2 settings to improve performance)")] + public List Kits { get; set; } + } + + public class WearItem + { + // In WearItem class + public WearItem() + { + ShortName = ""; + SkinID = 0; + } + + [JsonPropertyName("ShortName")] + public string ShortName { get; set; } + + [JsonPropertyName("SkinID (0 - default)")] + public long SkinID { get; set; } // Changed from int to long + } + + public class BeltItem + { + // In BeltItem class + public BeltItem() + { + ShortName = ""; + Amount = 0; + SkinID = 0; + Mods = new List(); + Ammo = ""; + } + + + [JsonPropertyName("ShortName")] + public string ShortName { get; set; } + + [JsonPropertyName("Amount")] + public int Amount { get; set; } + + [JsonPropertyName("SkinID (0 - default)")] + public long SkinID { get; set; } // Changed from int to long + + [JsonPropertyName("Mods")] + public List Mods { get; set; } + + [JsonPropertyName("Ammo")] + public string Ammo { get; set; } + } + + public class LootItem + { + // In LootItem class + public LootItem() + { + ShortName = ""; + Minimum = 0; + Maximum = 0; + Chance = 0.0; + IsBlueprint = false; + SkinID = 0; + Name = ""; + } + + [JsonPropertyName("ShortName")] + public string ShortName { get; set; } + + [JsonPropertyName("Minimum")] + public int Minimum { get; set; } + + [JsonPropertyName("Maximum")] + public int Maximum { get; set; } + + [JsonPropertyName("Chance [0.0-100.0]")] + public double Chance { get; set; } + + [JsonPropertyName("Is this a blueprint? [true/false]")] + public bool IsBlueprint { get; set; } + + [JsonPropertyName("SkinID (0 - default)")] + public long SkinID { get; set; } // Changed from int to long + + [JsonPropertyName("Name (empty - default)")] + public string Name { get; set; } + } + + public class EconomicsInfo + { + // In EconomicsInfo class + public EconomicsInfo() + { + Economics = 0.0; + ServerRewards = 0; + IQEconomic = 0; + } + + [JsonPropertyName("Economics")] + public double Economics { get; set; } + + [JsonPropertyName("Server Rewards (minimum 1)")] + public int ServerRewards { get; set; } + + [JsonPropertyName("IQEconomic (minimum 1)")] + public int IQEconomic { get; set; } + } + + public class PrefabLootTable + { + // In PrefabLootTable class + public PrefabLootTable() + { + MinPrefabs = 0; + MaxPrefabs = 0; + UseMinMax = false; + Prefabs = new List(); + } + + + [JsonPropertyName("Minimum numbers of prefabs")] + public int MinPrefabs { get; set; } + + [JsonPropertyName("Maximum numbers of prefabs")] + public int MaxPrefabs { get; set; } + + [JsonPropertyName("Use minimum and maximum values? [true/false]")] + public bool UseMinMax { get; set; } + + [JsonPropertyName("List of prefabs")] + public List Prefabs { get; set; } + } + + public class Prefab + { + // In Prefab class + public Prefab() + { + Chance = 0.0; + Path = ""; + } + + [JsonPropertyName("Chance [0.0-100.0]")] + public double Chance { get; set; } + + [JsonPropertyName("The path to the prefab")] + public string Path { get; set; } + } + + public class OwnLootTable + { + // In OwnLootTable class + public OwnLootTable() + { + MinItems = 0; + MaxItems = 0; + UseMinMax = false; + Items = new List(); + } + + [JsonPropertyName("Minimum numbers of items")] + public int MinItems { get; set; } + + [JsonPropertyName("Maximum numbers of items")] + public int MaxItems { get; set; } + + [JsonPropertyName("Use minimum and maximum values? [true/false]")] + public bool UseMinMax { get; set; } + + [JsonPropertyName("List of items")] + public List Items { get; set; } + } + +} diff --git a/Better NCP Editor/EditValueForm.cs b/Better NCP Editor/EditValueForm.cs new file mode 100644 index 0000000..3d4297b --- /dev/null +++ b/Better NCP Editor/EditValueForm.cs @@ -0,0 +1,97 @@ +using System; +using System.Drawing; +using System.Windows.Forms; + +namespace Better_NCP_Editor +{ + public partial class EditValueForm : Form + { + public object NewValue { get; private set; } + private Control inputControl; + + public EditValueForm(string propertyName, string currentValue, Type valueType) + { + // Set form properties. + this.Text = $"Edit {propertyName}"; + this.StartPosition = FormStartPosition.CenterParent; + this.ClientSize = new Size(250, 120); + + // Label for the property. + Label lblProperty = new Label() + { + Text = propertyName, + Location = new Point(10, 10), + AutoSize = true + }; + this.Controls.Add(lblProperty); + + // Create the appropriate input control. + if (valueType == typeof(bool)) + { + ComboBox combo = new ComboBox() + { + Location = new Point(10, 40), + Width = 200, + 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 = 200, + Text = currentValue + }; + inputControl = txtBox; + this.Controls.Add(txtBox); + } + + // OK button. + Button btnOk = new Button() + { + Text = "OK", + Location = new Point(10, 80), + DialogResult = DialogResult.OK + }; + btnOk.Click += BtnOk_Click; + this.Controls.Add(btnOk); + + // Cancel button. + Button btnCancel = new Button() + { + Text = "Cancel", + Location = new Point(120, 80), + DialogResult = DialogResult.Cancel + }; + 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) + { + NewValue = combo.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 + NewValue = txt.Text; + } + this.DialogResult = DialogResult.OK; + this.Close(); + } + } +} diff --git a/Better NCP Editor/EditValueForm.resx b/Better NCP Editor/EditValueForm.resx new file mode 100644 index 0000000..1af7de1 --- /dev/null +++ b/Better NCP Editor/EditValueForm.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/Better NCP Editor/Form1.Designer.cs b/Better NCP Editor/Form1.Designer.cs new file mode 100644 index 0000000..e188053 --- /dev/null +++ b/Better NCP Editor/Form1.Designer.cs @@ -0,0 +1,166 @@ +namespace Better_NCP_Editor +{ + partial class Form1 + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + btn_load = new Button(); + btn_save = new Button(); + dirTreeView = new TreeView(); + entityTreeView = new TreeView(); + btn_entity_del = new Button(); + btn_entity_add = new Button(); + btn_import_entityData = new Button(); + btn_export_entityData = new Button(); + statusTextbox = new TextBox(); + SuspendLayout(); + // + // btn_load + // + btn_load.Location = new Point(17, 12); + btn_load.Name = "btn_load"; + btn_load.Size = new Size(135, 29); + btn_load.TabIndex = 0; + btn_load.Text = "Load Directory"; + btn_load.UseVisualStyleBackColor = true; + btn_load.Click += btn_load_Click; + // + // btn_save + // + btn_save.Enabled = false; + btn_save.Location = new Point(214, 12); + btn_save.Name = "btn_save"; + btn_save.Size = new Size(93, 29); + btn_save.TabIndex = 1; + btn_save.Text = "Save File"; + btn_save.UseVisualStyleBackColor = true; + btn_save.Click += btn_save_Click; + // + // dirTreeView + // + dirTreeView.Location = new Point(17, 53); + dirTreeView.Name = "dirTreeView"; + dirTreeView.Size = new Size(290, 812); + dirTreeView.TabIndex = 2; + // + // entityTreeView + // + entityTreeView.Location = new Point(313, 53); + entityTreeView.Name = "entityTreeView"; + entityTreeView.Size = new Size(843, 812); + entityTreeView.TabIndex = 3; + // + // btn_entity_del + // + btn_entity_del.Enabled = false; + btn_entity_del.Location = new Point(366, 12); + btn_entity_del.Name = "btn_entity_del"; + btn_entity_del.Size = new Size(47, 29); + btn_entity_del.TabIndex = 8; + btn_entity_del.Text = "Del"; + btn_entity_del.UseVisualStyleBackColor = true; + btn_entity_del.Click += btn_entity_del_Click; + // + // btn_entity_add + // + btn_entity_add.Enabled = false; + btn_entity_add.Location = new Point(313, 12); + btn_entity_add.Name = "btn_entity_add"; + btn_entity_add.Size = new Size(46, 29); + btn_entity_add.TabIndex = 7; + btn_entity_add.Text = "Add"; + btn_entity_add.UseVisualStyleBackColor = true; + btn_entity_add.Click += btn_entity_add_Click; + // + // btn_import_entityData + // + btn_import_entityData.Enabled = false; + btn_import_entityData.Location = new Point(493, 12); + btn_import_entityData.Name = "btn_import_entityData"; + btn_import_entityData.Size = new Size(70, 29); + btn_import_entityData.TabIndex = 9; + btn_import_entityData.Text = "Import"; + btn_import_entityData.UseVisualStyleBackColor = true; + btn_import_entityData.Click += btn_import_entityData_Click; + // + // btn_export_entityData + // + btn_export_entityData.Enabled = false; + btn_export_entityData.Location = new Point(569, 12); + btn_export_entityData.Name = "btn_export_entityData"; + btn_export_entityData.Size = new Size(66, 29); + btn_export_entityData.TabIndex = 10; + btn_export_entityData.Text = "Export"; + btn_export_entityData.UseVisualStyleBackColor = true; + btn_export_entityData.Click += btn_export_entityData_Click; + // + // statusTextbox + // + statusTextbox.BorderStyle = BorderStyle.FixedSingle; + statusTextbox.ImeMode = ImeMode.NoControl; + statusTextbox.Location = new Point(660, 14); + statusTextbox.Name = "statusTextbox"; + statusTextbox.ReadOnly = true; + statusTextbox.Size = new Size(496, 27); + statusTextbox.TabIndex = 11; + // + // Form1 + // + AutoScaleDimensions = new SizeF(8F, 20F); + AutoScaleMode = AutoScaleMode.Font; + ClientSize = new Size(1176, 881); + Controls.Add(statusTextbox); + Controls.Add(btn_export_entityData); + Controls.Add(btn_import_entityData); + Controls.Add(btn_entity_del); + Controls.Add(btn_entity_add); + Controls.Add(entityTreeView); + Controls.Add(dirTreeView); + Controls.Add(btn_save); + Controls.Add(btn_load); + Name = "Form1"; + Text = "BetterNPC Editor V1.0"; + Load += Form1_Load; + ResumeLayout(false); + PerformLayout(); + } + + #endregion + + private Button btn_load; + private Button btn_save; + private TreeView dirTreeView; + private TreeView entityTreeView; + private Button btn_entity_del; + private Button btn_entity_add; + private Button btn_import_entityData; + private Button btn_export_entityData; + private TextBox statusTextbox; + } +} diff --git a/Better NCP Editor/Form1.cs b/Better NCP Editor/Form1.cs new file mode 100644 index 0000000..f7d13dd --- /dev/null +++ b/Better NCP Editor/Form1.cs @@ -0,0 +1,716 @@ +using System; +using System.IO; +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization.Metadata; +using System.Windows.Forms; + +namespace Better_NCP_Editor +{ + public partial class Form1 : Form + { + private string currentJsonFilePath; + private JsonNode currentJson; + private bool fileModified = false; + private ToolTips toolTips = new ToolTips(); + + // In your Form1 class: + private ToolTip customToolTip = new ToolTip(); + private TreeNode lastHoveredNode = null; + + public Form1() + { + InitializeComponent(); + dirTreeView.AfterSelect += DirTreeView_AfterSelect; + entityTreeView.NodeMouseDoubleClick += entityTreeView_NodeMouseDoubleClick; + entityTreeView.AfterSelect += entityTreeView_AfterSelect; + + // Prevent the form from shrinking below its current size. + this.MinimumSize = new Size(1045, 700); // Set minimum allowed size + this.FormBorderStyle = FormBorderStyle.Sizable; + + // Example layout using docking: + dirTreeView.Dock = DockStyle.Left; + entityTreeView.Dock = DockStyle.Fill; + + // Optionally, set a fixed width for the dirTreeView: + dirTreeView.Width = 290; + + // In the designer or constructor: + dirTreeView.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left; + entityTreeView.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; + entityTreeView.ShowNodeToolTips = false; + + // Configure the custom tooltip. + customToolTip.AutoPopDelay = 15000; // Show for 15 seconds. + customToolTip.AutomaticDelay = 1000; + customToolTip.ShowAlways = true; + customToolTip.InitialDelay = 500; + customToolTip.ReshowDelay = 500; + customToolTip.UseAnimation = true; + + // Subscribe to events. + entityTreeView.MouseMove += EntityTreeView_MouseMove; + entityTreeView.MouseLeave += EntityTreeView_MouseLeave; + + + } + private void EntityTreeView_MouseMove(object sender, MouseEventArgs e) + { + int xOffset = 15; + int yOffset = 0; + TreeNode node = entityTreeView.GetNodeAt(e.Location); + if (node != null) + { + // Measure the text size for the node using the TreeView's font. + Size textSize = TextRenderer.MeasureText(node.Text, entityTreeView.Font); + // Construct a rectangle that represents the area occupied by the text. + Rectangle textRect = new Rectangle(node.Bounds.X, node.Bounds.Y, textSize.Width, node.Bounds.Height); + + if (textRect.Contains(e.Location)) + { + if (node != lastHoveredNode) + { + lastHoveredNode = node; + // Use an offset so the tooltip doesn't overlap the text. + Point offsetLocation = new Point(e.Location.X + xOffset, e.Location.Y + xOffset); + customToolTip.Show(node.ToolTipText, entityTreeView, offsetLocation, customToolTip.AutoPopDelay); + } + } + else + { + customToolTip.Hide(entityTreeView); + lastHoveredNode = null; + } + } + else + { + customToolTip.Hide(entityTreeView); + lastHoveredNode = null; + } + } + + private void EntityTreeView_MouseLeave(object sender, EventArgs e) + { + customToolTip.Hide(entityTreeView); + lastHoveredNode = null; + } + + private void Form1_Load(object sender, EventArgs e) + { + // Other initialization code, if needed. + } + + private void btn_load_Click(object sender, EventArgs e) + { + using FolderBrowserDialog folderDialog = new(); + folderDialog.Description = "Select a directory containing JSON files"; + folderDialog.UseDescriptionForTitle = true; + folderDialog.ShowNewFolderButton = false; + + if (folderDialog.ShowDialog() == DialogResult.OK) + { + string selectedPath = folderDialog.SelectedPath; + dirTreeView.Nodes.Clear(); + + try + { + TreeNode rootNode = new TreeNode(Path.GetFileName(selectedPath)) + { + Tag = selectedPath + }; + dirTreeView.Nodes.Add(rootNode); + LoadJsonFiles(rootNode, selectedPath); + rootNode.Expand(); + statusTextbox.Text = $"Loaded directory: {selectedPath}"; + } + catch (Exception ex) + { + MessageBox.Show($"Error loading directory: {ex.Message}", "Error", + MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + } + + private void LoadJsonFiles(TreeNode parentNode, string directoryPath) + { + foreach (string filePath in Directory.GetFiles(directoryPath, "*.json")) + { + TreeNode fileNode = new TreeNode(Path.GetFileName(filePath)) + { + Tag = filePath + }; + parentNode.Nodes.Add(fileNode); + } + + foreach (string subDirPath in Directory.GetDirectories(directoryPath)) + { + TreeNode subDirNode = new TreeNode(Path.GetFileName(subDirPath)) + { + Tag = subDirPath + }; + parentNode.Nodes.Add(subDirNode); + LoadJsonFiles(subDirNode, subDirPath); + } + } + + private void DirTreeView_AfterSelect(object sender, TreeViewEventArgs e) + { + if (e.Node?.Tag is string filePath && + Path.GetExtension(filePath).Equals(".json", StringComparison.OrdinalIgnoreCase)) + { + LoadJsonFile(filePath); + } + } + + private void LoadJsonFile(string jsonFilePath) + { + try + { + currentJsonFilePath = jsonFilePath; + string json = File.ReadAllText(jsonFilePath); + currentJson = JsonNode.Parse(json); + PopulateEntityTree(currentJson); + fileModified = false; + btn_save.Enabled = false; // No changes yet + statusTextbox.Text = $"Loaded JSON file: {jsonFilePath}"; + } + catch (Exception ex) + { + MessageBox.Show($"Error loading JSON file: {ex.Message}", "Error", + MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + + + // Populates the entityTreeView recursively from a JsonNode. + private void PopulateEntityTree(JsonNode json) + { + entityTreeView.Nodes.Clear(); + // Use the filename of the current JSON file if available; otherwise default to "JSON". + string fileName = string.IsNullOrEmpty(currentJsonFilePath) ? "JSON" : Path.GetFileName(currentJsonFilePath); + TreeNode root = new TreeNode(fileName) + { + Tag = json + }; + entityTreeView.Nodes.Add(root); + PopulateTreeRecursive(json, root); + root.ExpandAll(); + entityTreeView.TopNode = root; + } + + private void PopulateTreeRecursive(JsonNode node, TreeNode treeNode) + { + if (node is JsonObject obj) + { + foreach (var kvp in obj) + { + TreeNode child; + if (kvp.Value is JsonValue) + { + child = new TreeNode($"{kvp.Key}: {kvp.Value}"); + } + else + { + child = new TreeNode(kvp.Key); + } + child.ToolTipText = toolTips.tips.ContainsKey(kvp.Key) ? toolTips.tips[kvp.Key] : ""; + child.Tag = kvp.Value; + treeNode.Nodes.Add(child); + PopulateTreeRecursive(kvp.Value, child); + } + } + else if (node is JsonArray arr) + { + for (int i = 0; i < arr.Count; i++) + { + JsonNode item = arr[i]; + TreeNode child; + if (item is JsonValue) + { + child = new TreeNode($"[{i}]: {item}"); + } + else + { + child = new TreeNode($"[{i}]"); + } + child.Tag = item; + treeNode.Nodes.Add(child); + PopulateTreeRecursive(item, child); + } + } + } + + private void entityTreeView_AfterSelect(object sender, TreeViewEventArgs e) + { + if (e.Node != null) + { + bool enableAddDelButtons = false; + bool enableImportExportButtons = false; + + // Enable if the selected node's Tag is a JsonArray + if (e.Node.Tag is JsonArray) + { + //enableButtons = true; + enableImportExportButtons = true; + } + // Or if the selected node is an item in an array (its parent is a JsonArray) + else if (e.Node.Parent != null && e.Node.Parent.Tag is JsonArray) + { + enableAddDelButtons = true; + enableImportExportButtons = true; + } + // You can also add additional conditions for a "preset" node, e.g. by checking text: + // else if (e.Node.Text.StartsWith("Preset", StringComparison.OrdinalIgnoreCase)) + // { + // enableButtons = true; + // } + + btn_entity_add.Enabled = enableAddDelButtons; + btn_entity_del.Enabled = enableAddDelButtons; + btn_export_entityData.Enabled = enableImportExportButtons; + btn_import_entityData.Enabled = enableImportExportButtons; + } + else + { + btn_entity_add.Enabled = false; + btn_entity_del.Enabled = false; + btn_export_entityData.Enabled = false; + btn_import_entityData.Enabled = false; + } + } + + // When a tree node is double-clicked, open an edit window for that value. + private void entityTreeView_NodeMouseDoubleClick(object sender, TreeNodeMouseClickEventArgs e) + { + if (e.Node == null) + return; + + // Expect node text in the form "Property: Value" + string[] parts = e.Node.Text.Split(new[] { ':' }, 2); + if (parts.Length != 2) + return; + + string propName = parts[0].Trim(); + string currentVal = parts[1].Trim(); + + // Use simple heuristics to determine the type. + Type valueType = typeof(string); + if (bool.TryParse(currentVal, out bool b)) + valueType = typeof(bool); + else if (int.TryParse(currentVal, out int i)) + valueType = typeof(int); + + using (EditValueForm editForm = new EditValueForm(propName, currentVal, valueType)) + { + if (editForm.ShowDialog() == DialogResult.OK) + { + object newVal = editForm.NewValue; + string displayVal = newVal is bool ? newVal.ToString().ToLower() : newVal.ToString(); + 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; + } + } + + + } + + + + private void btn_save_Click(object sender, EventArgs e) + { + if (string.IsNullOrEmpty(currentJsonFilePath) || currentJson == null) + { + MessageBox.Show("No JSON file loaded.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + return; + } + + try + { + var options = new JsonSerializerOptions + { + WriteIndented = true, + TypeInfoResolver = new DefaultJsonTypeInfoResolver(), + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }; + + string newJson = currentJson.ToJsonString(options); + File.WriteAllText(currentJsonFilePath, newJson); + statusTextbox.Text = "JSON file saved successfully."; + //MessageBox.Show("JSON file saved successfully.", "Success", MessageBoxButtons.OK, MessageBoxIcon.Information); + + // Reset the modified flag. + fileModified = false; + btn_save.Enabled = false; + } + catch (Exception ex) + { + MessageBox.Show($"Error saving JSON file: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + + + private TreeNode FindNodeByFullPath(TreeNodeCollection nodes, string fullPath) + { + foreach (TreeNode node in nodes) + { + if (node.FullPath == fullPath) + return node; + TreeNode found = FindNodeByFullPath(node.Nodes, fullPath); + if (found != null) + return found; + } + return null; + } + + private void btn_entity_add_Click(object sender, EventArgs e) + { + TreeNode selected = entityTreeView.SelectedNode; + if (selected == null) + { + MessageBox.Show("Please select a node to duplicate.", "Info", MessageBoxButtons.OK, MessageBoxIcon.Information); + return; + } + + TreeNode parent = selected.Parent; + if (parent == null) + { + MessageBox.Show("The selected node has no parent (cannot duplicate).", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + return; + } + + // Ensure the parent's Tag is a JsonArray. + if (!(parent.Tag is JsonArray parentArray)) + { + MessageBox.Show("The selected node's parent is not an array node.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + return; + } + + // Retrieve the underlying JsonNode for the selected node. + if (!(selected.Tag is JsonNode selectedJsonNode)) + { + MessageBox.Show("Selected node does not contain a valid JSON element.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + return; + } + + // Duplicate (deep clone) the selected JsonNode. + JsonNode duplicate = selectedJsonNode.DeepClone(); + + // Add the duplicate to the parent's array. + parentArray.Add(duplicate); + + // Refresh only the parent's children. + parent.Nodes.Clear(); + PopulateTreeRecursive((JsonNode)parent.Tag, parent); + parent.ExpandAll(); // Expand the parent and all its child nodes + + fileModified = true; + btn_save.Enabled = true; + + // Select the newly added item (last child in the parent's node collection). + if (parent.Nodes.Count > 0) + { + TreeNode newSelected = parent.Nodes[parent.Nodes.Count - 1]; + entityTreeView.SelectedNode = newSelected; + newSelected.EnsureVisible(); + entityTreeView.Focus(); + } + } + + private void btn_entity_del_Click(object sender, EventArgs e) + { + TreeNode selected = entityTreeView.SelectedNode; + if (selected == null) + { + MessageBox.Show("Please select a node to delete.", "Info", MessageBoxButtons.OK, MessageBoxIcon.Information); + return; + } + + TreeNode parent = selected.Parent; + if (parent == null) + { + MessageBox.Show("The selected node has no parent (cannot delete).", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + return; + } + + // Ensure the parent's Tag is a JsonArray. + if (!(parent.Tag is JsonArray parentArray)) + { + MessageBox.Show("The selected node's parent is not an array node.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + return; + } + + // Determine the index of the selected node (assumed to be in the same order as in the array). + int indexToRemove = selected.Index; + if (indexToRemove < 0 || indexToRemove >= parentArray.Count) + { + MessageBox.Show("Selected node index is invalid.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + return; + } + + // If this is the last item in the array, ask for confirmation. + if (parentArray.Count == 1) + { + DialogResult confirm = MessageBox.Show("Are you sure you want to delete the last item in the array?", + "Confirm Deletion", MessageBoxButtons.YesNo, MessageBoxIcon.Warning); + if (confirm != DialogResult.Yes) + { + return; + } + } + + // Remove the element from the underlying JsonArray. + parentArray.RemoveAt(indexToRemove); + + // Refresh only the parent's children. + parent.Nodes.Clear(); + PopulateTreeRecursive((JsonNode)parent.Tag, parent); + parent.ExpandAll(); + + fileModified = true; + btn_save.Enabled = true; + + // Select the newly last item in the array; if none, select the parent. + if (parent.Nodes.Count > 0) + { + TreeNode newSelected = parent.Nodes[parent.Nodes.Count - 1]; + entityTreeView.SelectedNode = newSelected; + newSelected.EnsureVisible(); + entityTreeView.Focus(); + } + else + { + entityTreeView.SelectedNode = parent; + parent.EnsureVisible(); + entityTreeView.Focus(); + } + } + + // Compare two JsonNode structures (objects, arrays, or values) for basic structural compatibility. + private bool CompareJsonStructure(JsonNode a, JsonNode b) + { + if (a == null || b == null) + return a == b; + + // Ensure both nodes are of the same concrete type. + if (a.GetType() != b.GetType()) + return false; + + if (a is JsonObject objA && b is JsonObject objB) + { + // They must have the same set of keys. + if (objA.Count != objB.Count) + return false; + foreach (var kvp in objA) + { + if (!objB.ContainsKey(kvp.Key)) + return false; + if (!CompareJsonStructure(kvp.Value, objB[kvp.Key])) + return false; + } + return true; + } + else if (a is JsonArray arrA && b is JsonArray arrB) + { + // If both arrays are empty, we consider them structurally compatible. + if (arrA.Count == 0 && arrB.Count == 0) + return true; + // Otherwise, compare the structure of their first elements. + if (arrA.Count > 0 && arrB.Count > 0) + return CompareJsonStructure(arrA[0], arrB[0]); + return false; + } + else if (a is JsonValue && b is JsonValue) + { + try + { + // Compare the underlying value types. + object valA = a.GetValue(); + object valB = b.GetValue(); + return valA?.GetType() == valB?.GetType(); + } + catch + { + return false; + } + } + return false; + } + + private void btn_import_entityData_Click(object sender, EventArgs e) + { + // Ensure a node is selected. + TreeNode selected = entityTreeView.SelectedNode; + if (selected == null) + { + MessageBox.Show("Please select a node for importing data.", "Import Error", MessageBoxButtons.OK, MessageBoxIcon.Information); + return; + } + + // Open a file picker to select the import file. + using (OpenFileDialog openDialog = new OpenFileDialog()) + { + openDialog.Filter = "JSON Files (*.json)|*.json|All Files (*.*)|*.*"; + openDialog.Title = "Import JSON Data"; + + if (openDialog.ShowDialog() == DialogResult.OK) + { + try + { + string importText = File.ReadAllText(openDialog.FileName); + JsonNode importJson = JsonNode.Parse(importText); + + // Determine if we are adding to an array or replacing a child. + // If the selected node's Tag is a JsonArray, we add to it. + // If the selected node's parent is a JsonArray, we replace the selected node. + JsonArray parentArray = null; + bool isAddingNewItem = false; + if (selected.Tag is JsonArray) + { + parentArray = (JsonArray)selected.Tag; + isAddingNewItem = true; + } + else if (selected.Parent != null && selected.Parent.Tag is JsonArray) + { + parentArray = (JsonArray)selected.Parent.Tag; + isAddingNewItem = false; + } + else + { + MessageBox.Show("The selected node is not part of an array. Import operation cancelled.", + "Import Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + return; + } + + // Determine the target node for structure comparison. + // If adding a new item, compare the structure of the array's first item (if any). + // Otherwise, compare the selected node's structure. + JsonNode targetStructure = null; + if (isAddingNewItem) + { + if (parentArray.Count > 0) + targetStructure = parentArray[0]; + else + { + // If the array is empty, we assume it's acceptable. + targetStructure = importJson; + } + } + else + { + targetStructure = (JsonNode)selected.Tag; + } + + if (!CompareJsonStructure(importJson, targetStructure)) + { + MessageBox.Show("The imported JSON structure does not match the target structure.", + "Import Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + return; + } + + fileModified = true; + btn_save.Enabled = true; + + if (isAddingNewItem) + { + // Add the imported JSON as a new item to the array. + parentArray.Add(importJson); + // Find the TreeNode corresponding to the array (selected node). + TreeNode arrayNode = selected; + // Refresh only this node's children. + arrayNode.Nodes.Clear(); + PopulateTreeRecursive((JsonNode)arrayNode.Tag, arrayNode); + arrayNode.ExpandAll(); + + // Select the newly added item (last child). + if (arrayNode.Nodes.Count > 0) + { + TreeNode newNode = arrayNode.Nodes[arrayNode.Nodes.Count - 1]; + entityTreeView.SelectedNode = newNode; + newNode.EnsureVisible(); + entityTreeView.Focus(); + } + } + else + { + // Replace the selected node's data with the imported JSON. + JsonNode oldNode = (JsonNode)selected.Tag; + oldNode.ReplaceWith(importJson); + selected.Tag = importJson; + + // Refresh the parent node. + TreeNode parentNode = selected.Parent; + parentNode.Nodes.Clear(); + PopulateTreeRecursive((JsonNode)parentNode.Tag, parentNode); + parentNode.ExpandAll(); + + // Try to reselect the replaced item (by its index). + int index = selected.Index; + if (index >= 0 && index < parentNode.Nodes.Count) + { + TreeNode newNode = parentNode.Nodes[index]; + entityTreeView.SelectedNode = newNode; + newNode.EnsureVisible(); + entityTreeView.Focus(); + } + } + } + catch (Exception ex) + { + MessageBox.Show($"Error importing JSON file: {ex.Message}", "Import Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + } + } + + private void btn_export_entityData_Click(object sender, EventArgs e) + { + // Ensure a node is selected and that it contains a JsonNode. + if (entityTreeView.SelectedNode == null || !(entityTreeView.SelectedNode.Tag is JsonNode selectedJson)) + { + MessageBox.Show("Please select a node with valid JSON data to export.", + "Export Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + return; + } + + using (SaveFileDialog saveDialog = new SaveFileDialog()) + { + saveDialog.Filter = "JSON Files (*.json)|*.json|All Files (*.*)|*.*"; + saveDialog.Title = "Export JSON Data"; + saveDialog.FileName = "exported.json"; + + if (saveDialog.ShowDialog() == DialogResult.OK) + { + try + { + var options = new JsonSerializerOptions + { + WriteIndented = true, + TypeInfoResolver = new DefaultJsonTypeInfoResolver(), + Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }; + + // Export the JSON subtree starting at the selected node. + string exportJson = selectedJson.ToJsonString(options); + File.WriteAllText(saveDialog.FileName, exportJson); + MessageBox.Show("Export successful!", "Export", MessageBoxButtons.OK, MessageBoxIcon.Information); + } + catch (Exception ex) + { + MessageBox.Show($"Error exporting JSON: {ex.Message}", "Export Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + } + } + } +} diff --git a/Better NCP Editor/Form1.resx b/Better NCP Editor/Form1.resx new file mode 100644 index 0000000..8b2ff64 --- /dev/null +++ b/Better NCP Editor/Form1.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/Better NCP Editor/Program.cs b/Better NCP Editor/Program.cs new file mode 100644 index 0000000..d157e8e --- /dev/null +++ b/Better NCP Editor/Program.cs @@ -0,0 +1,17 @@ +namespace Better_NCP_Editor +{ + internal static class Program + { + /// + /// The main entry point for the application. + /// + [STAThread] + static void Main() + { + // To customize application configuration such as set high DPI settings or default font, + // see https://aka.ms/applicationconfiguration. + ApplicationConfiguration.Initialize(); + Application.Run(new Form1()); + } + } +} \ No newline at end of file diff --git a/Better NCP Editor/ToolTips.cs b/Better NCP Editor/ToolTips.cs new file mode 100644 index 0000000..f8017da --- /dev/null +++ b/Better NCP Editor/ToolTips.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Better_NCP_Editor +{ + class ToolTips + { + public Dictionary tips { get; private set; } = new Dictionary(); + + + public ToolTips() + { + // General tips + tips.Add("Enabled? [true/false]", "Enables or disables NPC appearance on the map."); + tips.Add("Remove other NPCs? [true/false]", "Removes standard NPCs within the monument area."); + tips.Add("Radius", "NPC appearance radius."); + tips.Add("Use minimum and maximum values? [true/false]", "Whether to enforce minimum/maximum limits."); + + // Standard Monument tips + tips.Add("The size of the monument", "Specifies the monument's length and width for random NPC placement and standard NPC removal boundaries."); + + // Custom Monument tips + tips.Add("Position", "Custom monument position on the map."); + tips.Add("Rotation", "Custom monument rotation (needed for multi-map NPC placements)."); + + // NPC Preset tips + tips.Add("Minimum numbers - Day", "Minimum NPCs for the day preset."); + tips.Add("Maximum numbers - Day", "Maximum NPCs for the day preset."); + tips.Add("Minimum numbers - Night", "Minimum NPCs for the night preset."); + tips.Add("Maximum numbers - Night", "Maximum NPCs for the night preset."); + tips.Add("Type of appearance (0 - random; 1 - own list) (not used for Road and Biome)", "Specifies NPC appearance type: 0 for random, 1 for custom list. Not used for roads."); + tips.Add("Own list of locations (not used for Road and Biome)", "Custom list of NPC spawn locations. Ensure the list meets the maximum NPC count. Not used for roads."); + tips.Add("Which loot table should the plugin use? (0 - default; 1 - own; 2 - AlphaLoot; 3 - CustomLoot; 4 - loot table of the Rust objects; 5 - combine the 1 and 4 methods)", "Select the NPC loot table type. Type 5 combines types 1 and 4."); + tips.Add("Loot table from prefabs (if the loot table type is 4 or 5)", "Settings for the Rust loot table. See documentation for details."); + tips.Add("Own loot table (if the loot table type is 1 or 5)", "Custom NPC loot table. See documentation for details."); + + // NPC Settings tips + tips.Add("Names", "List of NPC names (chosen randomly)."); + tips.Add("Health", "NPC hit points."); + tips.Add("Roam Range", "Patrol distance from the spawn point."); + tips.Add("Chase Range", "Chase distance from the spawn point."); + tips.Add("Attack Range Multiplier", "Multiplier for the NPC's weapon range."); + tips.Add("Sense Range", "Target detection radius."); + tips.Add("Target Memory Duration [sec.]", "Time (in seconds) the NPC remembers a target."); + tips.Add("Scale damage", "Damage multiplier applied by the NPC."); + tips.Add("Aim Cone Scale", "Shooting spread (default in Rust is 2; non-negative only)."); + tips.Add("Detect the target only in the NPC's viewing vision cone? [true/false]", "If true, detection is limited to the NPC’s vision cone; false enables 360° detection."); + tips.Add("Vision Cone", "NPC vision cone angle (20–180°). Not used if detection is 360°."); + tips.Add("Speed", "NPC movement speed (default in Rust is 5)."); + tips.Add("Minimum time of appearance after death (not used for Events) [sec.]", "Minimum delay for NPC reappearance after death (not used for events)."); + tips.Add("Maximum time of appearance after death (not used for Events) [sec.]", "Maximum delay for NPC reappearance after death (not used for events)."); + tips.Add("Disable radio effects? [true/false]", "Toggle radio effects."); + tips.Add("Is this a stationary NPC? [true/false]", "If true, the NPC remains stationary."); + tips.Add("Remove a corpse after death? (it is recommended to use the true value to improve performance) [true/false]", "If true, NPC corpses are removed (recommended for performance)."); + tips.Add("Wear items", "List of NPC clothing and armor."); + tips.Add("Belt items", "List of quick-access items (e.g., weapons, medkits, grenades)."); + tips.Add("Kits (it is recommended to use the previous 2 settings to improve performance)", "List of NPC kits (leave blank if not used)."); + + // Rust loot table tips + tips.Add("Minimum numbers of prefabs", "Minimum number of prefabs in the loot table."); + tips.Add("Maximum numbers of prefabs", "Maximum number of prefabs in the loot table."); + + tips.Add("List of prefabs", "List of Rust object prefabs with full paths and drop chances."); + + // Own loot table tips + tips.Add("Minimum numbers of items", "Minimum number of items in the loot table."); + tips.Add("Maximum numbers of items", "Maximum number of items in the loot table."); + tips.Add("List of items", "List of NPC items, including blueprints and custom items."); + } + + + + + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..9829a30 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# BetterNPCEditor +