├── screenshots ├── bundler-new.png ├── bundler-fresh.png ├── bundler-imported.png └── bundler-detailview.png ├── EcoCivicsImportExportMod.Bundler ├── Icons │ ├── map.png │ ├── plus.png │ ├── user.png │ ├── cross.png │ ├── globe.png │ ├── money.png │ ├── money-bag.png │ ├── paper-bag.png │ ├── question.png │ ├── piggy-bank.png │ ├── user-green.png │ ├── auction-hammer.png │ ├── clipboard-list.png │ ├── user-business.png │ ├── paper-bag--plus.png │ ├── user-silhouette.png │ └── user-business-boss.png ├── App.xaml.cs ├── App.xaml ├── BundlerCommands.cs ├── AssemblyInfo.cs ├── View │ ├── CivicObjectView.xaml.cs │ ├── CivicObjectView.xaml │ ├── CivicObjectDetailView.xaml.cs │ ├── CivicBundleView.xaml.cs │ ├── CivicBundleView.xaml │ ├── MainWindow.xaml │ ├── CivicObjectDetailView.xaml │ └── MainWindow.xaml.cs ├── EcoTypes.cs ├── ViewModel │ ├── CivicReference.cs │ ├── MainWindow.cs │ ├── CivicBundle.cs │ └── CivicObject.cs ├── ObservableCollectionExt.cs ├── Icons.cs ├── EcoCivicsImportExportMod.Bundler.csproj ├── Model │ └── CivicBundle.cs └── Context.cs ├── .editorconfig ├── EcoCivicsImportExportMod ├── Migrations │ ├── 1000 │ │ └── MoneyLegalActionNamespace.cs │ ├── 1110 │ │ └── CustomStatUser.cs │ ├── ICivicsImpExpMigratorV1.cs │ ├── JExtensions.cs │ ├── ExternalMigratorV1.cs │ ├── 0973 │ │ └── LegalActionNamespace.cs │ ├── MigratorV1.cs │ └── 0950 │ │ ├── GameValueContext.cs │ │ └── CivicArticle.cs ├── GlobalSuppressions.cs ├── Registration.cs ├── Logger.cs ├── EcoCivicsImportExportMod.csproj ├── CivicsImpExpPlugin.cs ├── Exporter.cs ├── Importer.cs ├── CivicsJsonConverter.cs ├── CivicBundle.cs ├── ImportContext.cs └── CivicsImpExpCommands.cs ├── LICENSE ├── EcoCivicsImportExportMod.sln ├── .github └── workflows │ ├── staging.yml │ └── release.yml ├── .gitignore └── README.md /screenshots/bundler-new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasfn/EcoCivicsImportExportMod/HEAD/screenshots/bundler-new.png -------------------------------------------------------------------------------- /screenshots/bundler-fresh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasfn/EcoCivicsImportExportMod/HEAD/screenshots/bundler-fresh.png -------------------------------------------------------------------------------- /screenshots/bundler-imported.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasfn/EcoCivicsImportExportMod/HEAD/screenshots/bundler-imported.png -------------------------------------------------------------------------------- /screenshots/bundler-detailview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasfn/EcoCivicsImportExportMod/HEAD/screenshots/bundler-detailview.png -------------------------------------------------------------------------------- /EcoCivicsImportExportMod.Bundler/Icons/map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasfn/EcoCivicsImportExportMod/HEAD/EcoCivicsImportExportMod.Bundler/Icons/map.png -------------------------------------------------------------------------------- /EcoCivicsImportExportMod.Bundler/Icons/plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasfn/EcoCivicsImportExportMod/HEAD/EcoCivicsImportExportMod.Bundler/Icons/plus.png -------------------------------------------------------------------------------- /EcoCivicsImportExportMod.Bundler/Icons/user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasfn/EcoCivicsImportExportMod/HEAD/EcoCivicsImportExportMod.Bundler/Icons/user.png -------------------------------------------------------------------------------- /EcoCivicsImportExportMod.Bundler/Icons/cross.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasfn/EcoCivicsImportExportMod/HEAD/EcoCivicsImportExportMod.Bundler/Icons/cross.png -------------------------------------------------------------------------------- /EcoCivicsImportExportMod.Bundler/Icons/globe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasfn/EcoCivicsImportExportMod/HEAD/EcoCivicsImportExportMod.Bundler/Icons/globe.png -------------------------------------------------------------------------------- /EcoCivicsImportExportMod.Bundler/Icons/money.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasfn/EcoCivicsImportExportMod/HEAD/EcoCivicsImportExportMod.Bundler/Icons/money.png -------------------------------------------------------------------------------- /EcoCivicsImportExportMod.Bundler/Icons/money-bag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasfn/EcoCivicsImportExportMod/HEAD/EcoCivicsImportExportMod.Bundler/Icons/money-bag.png -------------------------------------------------------------------------------- /EcoCivicsImportExportMod.Bundler/Icons/paper-bag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasfn/EcoCivicsImportExportMod/HEAD/EcoCivicsImportExportMod.Bundler/Icons/paper-bag.png -------------------------------------------------------------------------------- /EcoCivicsImportExportMod.Bundler/Icons/question.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasfn/EcoCivicsImportExportMod/HEAD/EcoCivicsImportExportMod.Bundler/Icons/question.png -------------------------------------------------------------------------------- /EcoCivicsImportExportMod.Bundler/Icons/piggy-bank.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasfn/EcoCivicsImportExportMod/HEAD/EcoCivicsImportExportMod.Bundler/Icons/piggy-bank.png -------------------------------------------------------------------------------- /EcoCivicsImportExportMod.Bundler/Icons/user-green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasfn/EcoCivicsImportExportMod/HEAD/EcoCivicsImportExportMod.Bundler/Icons/user-green.png -------------------------------------------------------------------------------- /EcoCivicsImportExportMod.Bundler/Icons/auction-hammer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasfn/EcoCivicsImportExportMod/HEAD/EcoCivicsImportExportMod.Bundler/Icons/auction-hammer.png -------------------------------------------------------------------------------- /EcoCivicsImportExportMod.Bundler/Icons/clipboard-list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasfn/EcoCivicsImportExportMod/HEAD/EcoCivicsImportExportMod.Bundler/Icons/clipboard-list.png -------------------------------------------------------------------------------- /EcoCivicsImportExportMod.Bundler/Icons/user-business.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasfn/EcoCivicsImportExportMod/HEAD/EcoCivicsImportExportMod.Bundler/Icons/user-business.png -------------------------------------------------------------------------------- /EcoCivicsImportExportMod.Bundler/Icons/paper-bag--plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasfn/EcoCivicsImportExportMod/HEAD/EcoCivicsImportExportMod.Bundler/Icons/paper-bag--plus.png -------------------------------------------------------------------------------- /EcoCivicsImportExportMod.Bundler/Icons/user-silhouette.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasfn/EcoCivicsImportExportMod/HEAD/EcoCivicsImportExportMod.Bundler/Icons/user-silhouette.png -------------------------------------------------------------------------------- /EcoCivicsImportExportMod.Bundler/Icons/user-business-boss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasfn/EcoCivicsImportExportMod/HEAD/EcoCivicsImportExportMod.Bundler/Icons/user-business-boss.png -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.cs] 2 | 3 | # CA1416: Validate platform compatibility 4 | dotnet_diagnostic.CA1416.severity = silent 5 | 6 | # Indentation and spacing 7 | indent_size = 4 8 | indent_style = space 9 | tab_width = 4 10 | -------------------------------------------------------------------------------- /EcoCivicsImportExportMod/Migrations/ICivicsImpExpMigratorV1.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | using Newtonsoft.Json.Linq; 5 | 6 | namespace Eco.Mods.CivicsImpExp 7 | { 8 | public interface ICivicsImpExpMigratorV1 9 | { 10 | bool ShouldMigrate(JObject obj); 11 | 12 | bool ApplyMigration(JObject obj, IList outMigrationReport); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /EcoCivicsImportExportMod.Bundler/App.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Configuration; 4 | using System.Data; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | using System.Windows; 8 | 9 | namespace EcoCivicsImportExportMod.Bundler 10 | { 11 | /// 12 | /// Interaction logic for App.xaml 13 | /// 14 | public partial class App : Application 15 | { 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /EcoCivicsImportExportMod/GlobalSuppressions.cs: -------------------------------------------------------------------------------- 1 | // This file is used by Code Analysis to maintain SuppressMessage 2 | // attributes that are applied to this project. 3 | // Project-level suppressions either have no target or are given 4 | // a specific target and scoped to a namespace, type, member, etc. 5 | 6 | using System.Diagnostics.CodeAnalysis; 7 | 8 | [assembly: SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "")] 9 | -------------------------------------------------------------------------------- /EcoCivicsImportExportMod.Bundler/App.xaml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /EcoCivicsImportExportMod/Registration.cs: -------------------------------------------------------------------------------- 1 | namespace Eco.Mods.CivicsImpExp 2 | { 3 | using Core.Plugins.Interfaces; 4 | 5 | public class CivicsImpExpMod : IModInit 6 | { 7 | public static ModRegistration Register() => new() 8 | { 9 | ModName = "CivicsImportExport", 10 | ModDescription = "Adds admin-only commands that allow exporting and importing civics across worlds.", 11 | ModDisplayName = "Civics Import Export", 12 | }; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /EcoCivicsImportExportMod.Bundler/BundlerCommands.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Windows.Input; 3 | 4 | namespace EcoCivicsImportExportMod.Bundler 5 | { 6 | public static class BundlerCommands 7 | { 8 | public static readonly RoutedUICommand AddToBundle = new RoutedUICommand 9 | ( 10 | "Add to Bundle", 11 | "AddToBundle", 12 | typeof(BundlerCommands) 13 | ); 14 | 15 | public static readonly RoutedUICommand RemoveFromBundle = new RoutedUICommand 16 | ( 17 | "Remove from Bundle", 18 | "RemoveFromBundle", 19 | typeof(BundlerCommands) 20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /EcoCivicsImportExportMod.Bundler/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | 3 | [assembly: ThemeInfo( 4 | ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located 5 | //(used if a resource is not found in the page, 6 | // or application resource dictionaries) 7 | ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located 8 | //(used if a resource is not found in the page, 9 | // app, or any theme specific resource dictionaries) 10 | )] 11 | -------------------------------------------------------------------------------- /EcoCivicsImportExportMod/Logger.cs: -------------------------------------------------------------------------------- 1 | namespace Eco.Mods.CivicsImpExp 2 | { 3 | using Shared.Logging; 4 | using Shared.Localization; 5 | 6 | public static class Logger 7 | { 8 | public static void Debug(string message) 9 | { 10 | Log.Write(new LocString("[CivicsImpExpPlugin] DEBUG: " + message + "\n")); 11 | } 12 | 13 | public static void Info(string message) 14 | { 15 | Log.Write(new LocString("[CivicsImpExpPlugin] " + message + "\n")); 16 | } 17 | 18 | public static void Error(string message) 19 | { 20 | Log.Write(new LocString("[CivicsImpExpPlugin] ERROR: " + message + "\n")); 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /EcoCivicsImportExportMod/EcoCivicsImportExportMod.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | Eco.Mods.CivicsImpExp 6 | 7 | 8 | 9 | x64 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /EcoCivicsImportExportMod.Bundler/View/CivicObjectView.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using System.Windows; 7 | using System.Windows.Controls; 8 | using System.Windows.Data; 9 | using System.Windows.Documents; 10 | using System.Windows.Input; 11 | using System.Windows.Media; 12 | using System.Windows.Media.Imaging; 13 | using System.Windows.Navigation; 14 | using System.Windows.Shapes; 15 | 16 | namespace EcoCivicsImportExportMod.Bundler.View 17 | { 18 | /// 19 | /// Interaction logic for CivicObjectView.xaml 20 | /// 21 | public partial class CivicObjectView : UserControl 22 | { 23 | public CivicObjectView() 24 | { 25 | InitializeComponent(); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /EcoCivicsImportExportMod/CivicsImpExpPlugin.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.IO; 4 | using System.Collections.Generic; 5 | 6 | namespace Eco.Mods.CivicsImpExp 7 | { 8 | using Core.Plugins.Interfaces; 9 | using Core.Utils; 10 | using Core.Systems; 11 | 12 | using Shared.Utils; 13 | 14 | public class CivicsImpExpPlugin : Singleton, IModKitPlugin, IInitializablePlugin 15 | { 16 | public const string ImportExportDirectory = "civics"; 17 | 18 | public List LastImport { get; } = new List(); 19 | 20 | public string GetStatus() 21 | { 22 | return "Idle"; 23 | } 24 | 25 | public string GetCategory() 26 | { 27 | return "Civics"; 28 | } 29 | 30 | public void Initialize(TimedTask timer) 31 | { 32 | Directory.CreateDirectory(ImportExportDirectory); 33 | Logger.Info("Initialized and ready to go"); 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /EcoCivicsImportExportMod/Exporter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Text; 4 | using System.Net.Http; 5 | using System.Threading.Tasks; 6 | 7 | using Newtonsoft.Json; 8 | 9 | namespace Eco.Mods.CivicsImpExp 10 | { 11 | using Core.Systems; 12 | 13 | public static class Exporter 14 | { 15 | private static readonly HttpClient httpClient = new HttpClient(); 16 | 17 | public static async Task Export(IHasUniversalID civicObject, string destination) 18 | { 19 | string text = JsonConvert.SerializeObject(civicObject, Formatting.Indented, new CivicsJsonConverter()); 20 | if (Uri.TryCreate(destination, UriKind.Absolute, out Uri uri)) 21 | { 22 | await httpClient.PostAsync(uri, new StringContent(text, Encoding.UTF8, "application/json")); 23 | } 24 | else 25 | { 26 | await File.WriteAllTextAsync(destination, text); 27 | } 28 | 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /EcoCivicsImportExportMod/Migrations/JExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | using Newtonsoft.Json.Linq; 4 | 5 | namespace Eco.Mods.CivicsImpExp.Migrations 6 | { 7 | public static class JExtensions 8 | { 9 | public static IEnumerable GetNestedObjects(this JObject obj) 10 | { 11 | foreach (var pair in obj) 12 | { 13 | if (pair.Value is JObject innerObj) 14 | { 15 | yield return innerObj; 16 | foreach (var x in GetNestedObjects(innerObj)) { yield return x; } 17 | } 18 | else if (pair.Value is JArray innerArr) 19 | { 20 | foreach (var element in innerArr) 21 | { 22 | if (element is JObject elementObj) 23 | { 24 | foreach (var x in GetNestedObjects(elementObj)) { yield return x; } 25 | } 26 | } 27 | } 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 thomasfn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /EcoCivicsImportExportMod.Bundler/View/CivicObjectView.xaml: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /EcoCivicsImportExportMod.Bundler/EcoTypes.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace EcoCivicsImportExportMod.Bundler 4 | { 5 | public static class EcoTypes 6 | { 7 | public const string ConstitutionalAmendment = "Eco.Gameplay.Civics.Constitutional.ConstitutionalAmendment"; 8 | public const string Constitution = "Eco.Gameplay.Civics.Constitution"; 9 | public const string Demographic = "Eco.Gameplay.Civics.Demographics.Demographic"; 10 | public const string District = "Eco.Gameplay.LegislationSystem.District"; 11 | public const string DistrictMap = "Eco.Gameplay.Civics.Districts.DistrictMap"; 12 | public const string ElectedTitle = "Eco.Gameplay.Civics.Titles.ElectedTitle"; 13 | public const string ElectionProcess = "Eco.Gameplay.Civics.ElectionProcess"; 14 | public const string Law = "Eco.Gameplay.Civics.Laws.Law"; 15 | 16 | public const string AppointedTitle = "Eco.Gameplay.Civics.Titles.AppointedTitle"; 17 | public const string Currency = "Eco.Gameplay.Economy.Currency"; 18 | public const string GovernmentAccount = "Eco.Gameplay.Economy.GovernmentBankAccount"; 19 | public const string PersonalAccount = "Eco.Gameplay.Economy.PersonalAccount"; 20 | public const string User = "Eco.Gameplay.Players.User"; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /EcoCivicsImportExportMod/Migrations/ExternalMigratorV1.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Reflection; 4 | 5 | using Newtonsoft.Json.Linq; 6 | 7 | namespace Eco.Mods.CivicsImpExp.Migrations 8 | { 9 | public class ExternalMigratorV1 : ICivicsImpExpMigratorV1 10 | { 11 | private readonly object migratorObj; 12 | private readonly MethodInfo shouldMigrateMethod; 13 | private readonly MethodInfo applyMigrationMethod; 14 | 15 | public ExternalMigratorV1(object migratorObj) 16 | { 17 | this.migratorObj = migratorObj; 18 | shouldMigrateMethod = migratorObj.GetType().GetMethod("ShouldMigrate", BindingFlags.Public | BindingFlags.Instance); 19 | applyMigrationMethod = migratorObj.GetType().GetMethod("ApplyMigration", BindingFlags.Public | BindingFlags.Instance); 20 | } 21 | 22 | public bool ShouldMigrate(JObject obj) 23 | => (bool)shouldMigrateMethod.Invoke(migratorObj, new object[] { obj }); 24 | 25 | public bool ApplyMigration(JObject obj, IList outMigrationReport) 26 | => (bool)applyMigrationMethod.Invoke(migratorObj, new object[] { obj, outMigrationReport }); 27 | 28 | public override string ToString() 29 | => $"ExternalMigratorV1({migratorObj})"; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /EcoCivicsImportExportMod.Bundler/ViewModel/CivicReference.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | 4 | namespace EcoCivicsImportExportMod.Bundler.ViewModel 5 | { 6 | public class CivicReference : INotifyPropertyChanged 7 | { 8 | private Model.CivicReference underlyingCivicReference; 9 | 10 | public Model.CivicReference UnderlyingCivicReference 11 | { 12 | get => underlyingCivicReference; 13 | set 14 | { 15 | if (value == underlyingCivicReference) { return; } 16 | underlyingCivicReference = value; 17 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(UnderlyingCivicReference))); 18 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name))); 19 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FullType))); 20 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IconSource))); 21 | } 22 | } 23 | 24 | public string Name { get => underlyingCivicReference.Name; } 25 | 26 | public string FullType { get => underlyingCivicReference.Type; } 27 | 28 | public string IconSource { get => Icons.TypeToIconSource(FullType); } 29 | 30 | public event PropertyChangedEventHandler PropertyChanged; 31 | 32 | public CivicReference(Model.CivicReference underlyingCivicReference) 33 | { 34 | this.underlyingCivicReference = underlyingCivicReference; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /EcoCivicsImportExportMod.Bundler/ObservableCollectionExt.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | using System.ComponentModel; 5 | 6 | namespace EcoCivicsImportExportMod.Bundler 7 | { 8 | public delegate TViewModel CreateViewModelFromModel(in TModel model); 9 | 10 | public delegate void UpdateViewModelFromModel(TViewModel viewModel, in TModel model); 11 | 12 | public static class ObservableCollectionExt 13 | { 14 | public static void SetFromEnumerable( 15 | this ObservableCollection observableCollection, IEnumerable items, 16 | CreateViewModelFromModel createViewModelFromModel, 17 | UpdateViewModelFromModel updateViewModelFromModel 18 | ) 19 | where TViewModel : INotifyPropertyChanged 20 | { 21 | int ptr = 0; 22 | foreach (var item in items) 23 | { 24 | if (ptr >= observableCollection.Count) 25 | { 26 | observableCollection.Add(createViewModelFromModel(item)); 27 | } 28 | else 29 | { 30 | updateViewModelFromModel(observableCollection[ptr], item); 31 | } 32 | ++ptr; 33 | } 34 | while (observableCollection.Count > ptr) 35 | { 36 | observableCollection.RemoveAt(ptr); 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /EcoCivicsImportExportMod/Importer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Net.Http; 4 | using System.Threading.Tasks; 5 | using System.Collections.Generic; 6 | 7 | namespace Eco.Mods.CivicsImpExp 8 | { 9 | using Core.Systems; 10 | 11 | using Gameplay.Civics.Misc; 12 | 13 | public static class Importer 14 | { 15 | private static readonly HttpClient httpClient = new HttpClient(); 16 | 17 | public static async Task ImportBundle(string source) 18 | { 19 | string text; 20 | if (Uri.TryCreate(source, UriKind.Absolute, out Uri uri)) 21 | { 22 | text = await httpClient.GetStringAsync(uri); 23 | } 24 | else 25 | { 26 | text = await File.ReadAllTextAsync(Path.Combine(CivicsImpExpPlugin.ImportExportDirectory, source)); 27 | } 28 | return CivicBundle.LoadFromText(text); 29 | } 30 | 31 | public static void Cleanup(IHasID obj) 32 | { 33 | Registrars.GetByDerivedType(obj.GetType()).Remove(obj); 34 | if (obj is IHasSubRegistrarEntries hasSubRegistrarEntries) 35 | { 36 | foreach (var subObj in hasSubRegistrarEntries.SubRegistrarEntries) 37 | { 38 | Cleanup(subObj); 39 | } 40 | } 41 | } 42 | 43 | public static void Cleanup(IEnumerable objs) 44 | { 45 | foreach (var obj in objs) 46 | { 47 | Cleanup(obj); 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /EcoCivicsImportExportMod/Migrations/1110/CustomStatUser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | using Newtonsoft.Json.Linq; 6 | 7 | namespace Eco.Mods.CivicsImpExp.Migrations._1110 8 | { 9 | using Shared.Utils; 10 | 11 | public class CustomStatUser : ICivicsImpExpMigratorV1 12 | { 13 | private const string CustomStatTypeName = "Eco.Gameplay.Civics.GameValues.Values.Stats.CustomStatQuery"; 14 | private const string OldUserKey = "User"; 15 | private const string NewUserKey = "RestrictCountToCitizen"; 16 | 17 | public bool ShouldMigrate(JObject obj) 18 | => GetMigrateTargets(obj) 19 | .Any(); 20 | 21 | private static IEnumerable GetMigrateTargets(JObject obj) 22 | => obj.GetNestedObjects().Where(x => x.Value("type") == CustomStatTypeName); 23 | 24 | public bool ApplyMigration(JObject obj, IList outMigrationReport) 25 | { 26 | bool didMigrate = false; 27 | foreach (var customStatQueryObj in GetMigrateTargets(obj)) 28 | { 29 | var propertiesObj = customStatQueryObj.Value("properties"); 30 | if (propertiesObj == null) { continue; } 31 | 32 | var userValue = propertiesObj.Value(OldUserKey); 33 | if (userValue == null) { continue; } 34 | 35 | propertiesObj[NewUserKey] = userValue; 36 | propertiesObj.Remove(OldUserKey); 37 | 38 | outMigrationReport.Add($"Changed '{OldUserKey}' to '{NewUserKey}'"); 39 | didMigrate = true; 40 | } 41 | return didMigrate; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /EcoCivicsImportExportMod.Bundler/View/CivicObjectDetailView.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using System.Windows; 7 | using System.Windows.Controls; 8 | using System.Windows.Data; 9 | using System.Windows.Documents; 10 | using System.Windows.Input; 11 | using System.Windows.Media; 12 | using System.Windows.Media.Imaging; 13 | using System.Windows.Navigation; 14 | using System.Windows.Shapes; 15 | 16 | namespace EcoCivicsImportExportMod.Bundler.View 17 | { 18 | /// 19 | /// Interaction logic for CivicObjectDetailView.xaml 20 | /// 21 | public partial class CivicObjectDetailView : UserControl 22 | { 23 | public static readonly RoutedUICommand UpdateTextBoxBindingOnEnterCommand = new RoutedUICommand 24 | ( 25 | "Enter", 26 | "Enter", 27 | typeof(CivicObjectDetailView) 28 | ); 29 | 30 | public CivicObjectDetailView() 31 | { 32 | InitializeComponent(); 33 | } 34 | 35 | private void CanExecuteUpdateTextBoxBindingOnEnterCommand(object sender, CanExecuteRoutedEventArgs e) => e.CanExecute = true; 36 | 37 | private void ExecuteUpdateTextBoxBindingOnEnterCommand(object sender, ExecutedRoutedEventArgs e) 38 | { 39 | TextBox tBox = e.Parameter as TextBox; 40 | if (tBox != null) 41 | { 42 | DependencyProperty prop = TextBox.TextProperty; 43 | BindingExpression binding = BindingOperations.GetBindingExpression(tBox, prop); 44 | if (binding != null) 45 | binding.UpdateSource(); 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /EcoCivicsImportExportMod.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30711.63 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EcoCivicsImportExportMod", "EcoCivicsImportExportMod\EcoCivicsImportExportMod.csproj", "{494C591F-1503-4E1B-8F6A-1244EFDD3094}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EcoCivicsImportExportMod.Bundler", "EcoCivicsImportExportMod.Bundler\EcoCivicsImportExportMod.Bundler.csproj", "{927220AD-E5EA-4FB2-8C99-2D3DBC55174C}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {494C591F-1503-4E1B-8F6A-1244EFDD3094}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {494C591F-1503-4E1B-8F6A-1244EFDD3094}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {494C591F-1503-4E1B-8F6A-1244EFDD3094}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {494C591F-1503-4E1B-8F6A-1244EFDD3094}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {927220AD-E5EA-4FB2-8C99-2D3DBC55174C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {927220AD-E5EA-4FB2-8C99-2D3DBC55174C}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {927220AD-E5EA-4FB2-8C99-2D3DBC55174C}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {927220AD-E5EA-4FB2-8C99-2D3DBC55174C}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {48279F27-0E65-4350-B32C-11C3801A62B0} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /EcoCivicsImportExportMod/Migrations/0973/LegalActionNamespace.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | using Newtonsoft.Json.Linq; 6 | 7 | namespace Eco.Mods.CivicsImpExp.Migrations._0973 8 | { 9 | using Shared.Utils; 10 | 11 | public class LegalActionNamespace : ICivicsImpExpMigratorV1 12 | { 13 | public bool ShouldMigrate(JObject obj) 14 | => GetVanillaLegalActions(obj) 15 | .Any(); 16 | 17 | private bool IsVanillaLegalAction(string typeName) 18 | => !string.IsNullOrEmpty(typeName) && ((typeName.StartsWith("Eco.Gameplay.Civics.") && typeName.EndsWith("_LegalAction")) || typeName == "Eco.Gameplay.Civics.SendNotice"); 19 | 20 | private IEnumerable GetVanillaLegalActions(JObject obj) 21 | => obj.GetNestedObjects().Where(x => IsVanillaLegalAction(x.Value("type"))); 22 | 23 | public bool ApplyMigration(JObject obj, IList outMigrationReport) 24 | { 25 | bool didMigrate = false; 26 | foreach (var legalActionObj in GetVanillaLegalActions(obj)) 27 | { 28 | var oldType = legalActionObj.Value("type"); 29 | if (!oldType.StartsWith("Eco.Gameplay.Civics.")) { Logger.Debug($"Skipping (not a legal action)"); continue; } 30 | if (oldType.StartsWith("Eco.Gameplay.Civics.LegalActions.")) { Logger.Debug($"Skipping (already migrated)"); continue; } 31 | var fixedType = $"Eco.Gameplay.Civics.LegalActions.{oldType["Eco.Gameplay.Civics.".Length..]}"; 32 | legalActionObj["type"] = fixedType; 33 | outMigrationReport.Add($"Changed '{oldType}' to '{fixedType}'"); 34 | didMigrate = true; 35 | } 36 | return didMigrate; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /EcoCivicsImportExportMod/Migrations/1000/MoneyLegalActionNamespace.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | using Newtonsoft.Json.Linq; 6 | 7 | namespace Eco.Mods.CivicsImpExp.Migrations._1000 8 | { 9 | using Shared.Utils; 10 | 11 | public class MoneyLegalActionNamespace : ICivicsImpExpMigratorV1 12 | { 13 | private static readonly Dictionary typeRenames = new() 14 | { 15 | { "Eco.Gameplay.Civics.LegalActions.TransferToAccount_LegalAction", "Eco.Gameplay.Civics.Laws.LegalActions.Money.TransferToAccount_LegalAction" }, 16 | { "Eco.Gameplay.Civics.LegalActions.Pay_LegalAction", "Eco.Gameplay.Civics.Laws.LegalActions.Money.Pay_LegalAction" }, 17 | { "Eco.Gameplay.Civics.LegalActions.Tax_LegalAction", "Eco.Gameplay.Civics.Laws.LegalActions.Money.Tax_LegalAction" } 18 | }; 19 | 20 | public bool ShouldMigrate(JObject obj) 21 | => GetRenameTargets(obj) 22 | .Any(); 23 | 24 | private bool IsRenameTarget(string typeName) 25 | => !string.IsNullOrEmpty(typeName) && typeRenames.ContainsKey(typeName); 26 | 27 | private IEnumerable GetRenameTargets(JObject obj) 28 | => obj.GetNestedObjects().Where(x => IsRenameTarget(x.Value("type"))); 29 | 30 | public bool ApplyMigration(JObject obj, IList outMigrationReport) 31 | { 32 | bool didMigrate = false; 33 | foreach (var legalActionObj in GetRenameTargets(obj)) 34 | { 35 | var oldType = legalActionObj.Value("type"); 36 | if (!typeRenames.TryGetValue(oldType, out var newType)) { continue; } 37 | legalActionObj["type"] = newType; 38 | outMigrationReport.Add($"Changed '{oldType}' to '{newType}'"); 39 | didMigrate = true; 40 | } 41 | return didMigrate; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /EcoCivicsImportExportMod.Bundler/Icons.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace EcoCivicsImportExportMod.Bundler 5 | { 6 | public static class Icons 7 | { 8 | private static readonly IReadOnlyDictionary typesToIconSources = new Dictionary 9 | { 10 | { EcoTypes.ConstitutionalAmendment, "/EcoCivicsImportExportMod.Bundler;component/Icons/paper-bag--plus.png" }, 11 | { EcoTypes.Constitution, "/EcoCivicsImportExportMod.Bundler;component/Icons/paper-bag.png" }, 12 | { EcoTypes.Demographic, "/EcoCivicsImportExportMod.Bundler;component/Icons/user-silhouette.png" }, 13 | { EcoTypes.District, "/EcoCivicsImportExportMod.Bundler;component/Icons/map.png" }, 14 | { EcoTypes.DistrictMap, "/EcoCivicsImportExportMod.Bundler;component/Icons/globe.png" }, 15 | { EcoTypes.ElectedTitle, "/EcoCivicsImportExportMod.Bundler;component/Icons/user-business-boss.png" }, 16 | { EcoTypes.ElectionProcess, "/EcoCivicsImportExportMod.Bundler;component/Icons/clipboard-list.png" }, 17 | { EcoTypes.Law, "/EcoCivicsImportExportMod.Bundler;component/Icons/auction-hammer.png" }, 18 | 19 | { EcoTypes.AppointedTitle, "/EcoCivicsImportExportMod.Bundler;component/Icons/user-green.png" }, 20 | { EcoTypes.Currency, "/EcoCivicsImportExportMod.Bundler;component/Icons/money.png" }, 21 | { EcoTypes.PersonalAccount, "/EcoCivicsImportExportMod.Bundler;component/Icons/piggy-bank.png" }, 22 | { EcoTypes.GovernmentAccount, "/EcoCivicsImportExportMod.Bundler;component/Icons/money-bag.png" }, 23 | { EcoTypes.User, "/EcoCivicsImportExportMod.Bundler;component/Icons/user.png" } 24 | }; 25 | 26 | private const string unknownTypeIcon = "/EcoCivicsImportExportMod.Bundler;component/Icons/question.png"; 27 | 28 | public static string TypeToIconSource(string type) 29 | => typesToIconSources.TryGetValue(type, out var result) ? result : unknownTypeIcon; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.github/workflows/staging.yml: -------------------------------------------------------------------------------- 1 | name: build for staging 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'staging' 7 | 8 | jobs: 9 | build-mod: 10 | runs-on: ubuntu-latest 11 | env: 12 | ECO_BRANCH: staging 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Setup .NET Core 8.0 16 | uses: actions/setup-dotnet@v4 17 | with: 18 | dotnet-version: '8.0.x' 19 | - name: Fetch dependencies 20 | run: dotnet restore ./EcoCivicsImportExportMod/EcoCivicsImportExportMod.csproj 21 | env: 22 | DOTNET_SYSTEM_GLOBALIZATION_INVARIANT: 1 23 | - name: Build 24 | run: dotnet build ./EcoCivicsImportExportMod/EcoCivicsImportExportMod.csproj --configuration Release --no-restore 25 | env: 26 | DOTNET_SYSTEM_GLOBALIZATION_INVARIANT: 1 27 | - name: Upload build artifact 28 | uses: actions/upload-artifact@v4 29 | with: 30 | name: mod-binaries-staging 31 | path: EcoCivicsImportExportMod/bin/Release/net8.0/EcoCivicsImportExportMod.* 32 | build-bundler-tool: 33 | runs-on: windows-latest 34 | steps: 35 | - uses: actions/checkout@v4 36 | - name: Setup .NET Core 5.0 37 | uses: actions/setup-dotnet@v4 38 | with: 39 | dotnet-version: '5.0.201' 40 | - name: Fetch dependencies 41 | run: dotnet restore ./EcoCivicsImportExportMod.Bundler/EcoCivicsImportExportMod.Bundler.csproj 42 | env: 43 | DOTNET_SYSTEM_GLOBALIZATION_INVARIANT: 1 44 | - name: Build 45 | run: dotnet build ./EcoCivicsImportExportMod.Bundler/EcoCivicsImportExportMod.Bundler.csproj --configuration Release --no-restore 46 | env: 47 | DOTNET_SYSTEM_GLOBALIZATION_INVARIANT: 1 48 | - name: Archive 49 | run: 7z a EcoCivicsImportExportMod.Bundler/bin/EcoCivicsImportExportMod.Bundler.zip ./EcoCivicsImportExportMod.Bundler/bin/Release/net5.0-windows/* 50 | - name: Upload build artifact 51 | uses: actions/upload-artifact@v4 52 | with: 53 | name: bundler-tool-binaries-staging 54 | path: EcoCivicsImportExportMod.Bundler/bin/EcoCivicsImportExportMod.Bundler.zip 55 | -------------------------------------------------------------------------------- /EcoCivicsImportExportMod/Migrations/MigratorV1.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using System.Reflection; 7 | 8 | using Newtonsoft.Json.Linq; 9 | 10 | namespace Eco.Mods.CivicsImpExp.Migrations 11 | { 12 | using Shared.Utils; 13 | 14 | public class MigratorV1 : ICivicsImpExpMigratorV1 15 | { 16 | private static IEnumerable InternalMigrators 17 | => typeof(ICivicsImpExpMigratorV1).ConcreteTypesWithInteface() 18 | .Except(new Type[] { typeof(MigratorV1), typeof(ExternalMigratorV1) }) 19 | .Select(t => Activator.CreateInstance(t) as ICivicsImpExpMigratorV1); 20 | 21 | private static IEnumerable ExternalMigrators 22 | => ReflectionCache 23 | .GetGameAssemblies() 24 | .SelectMany(a => a.GetTypes()) 25 | .Where(t => t.GetInterface("ICivicsImpExpMigratorV1") != null && !t.IsAssignableTo(typeof(ICivicsImpExpMigratorV1))) 26 | .Select(t => new ExternalMigratorV1(Activator.CreateInstance(t))); 27 | 28 | private readonly IEnumerable allMigrators = ExternalMigrators.Concat(InternalMigrators); 29 | 30 | public bool ShouldMigrate(JObject obj) 31 | { 32 | foreach (var migrator in allMigrators) 33 | { 34 | if (migrator.ShouldMigrate(obj)) 35 | { 36 | return true; 37 | } 38 | } 39 | return false; 40 | } 41 | 42 | public bool ApplyMigration(JObject obj, IList outMigrationReport) 43 | { 44 | bool didMigrate = false; 45 | foreach (var migrator in allMigrators) 46 | { 47 | if (migrator.ShouldMigrate(obj)) 48 | { 49 | didMigrate |= migrator.ApplyMigration(obj, outMigrationReport); 50 | } 51 | } 52 | return didMigrate; 53 | } 54 | 55 | 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /EcoCivicsImportExportMod.Bundler/EcoCivicsImportExportMod.Bundler.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | WinExe 5 | net5.0-windows 6 | true 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /EcoCivicsImportExportMod/Migrations/0950/GameValueContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using System.Reflection; 7 | 8 | using Newtonsoft.Json.Linq; 9 | 10 | namespace Eco.Mods.CivicsImpExp.Migrations._0950 11 | { 12 | using Shared.Utils; 13 | 14 | public class GameValueContext : ICivicsImpExpMigratorV1 15 | { 16 | public bool ShouldMigrate(JObject obj) 17 | => GetGameValueContexts(obj) 18 | .Any(x => !string.IsNullOrEmpty(x.Value("contextName")) || !string.IsNullOrEmpty(x.Value("titleBacking")) || !string.IsNullOrEmpty(x.Value("tooltip"))); 19 | 20 | private IEnumerable GetGameValueContexts(JObject obj) 21 | => obj.GetNestedObjects().Where(x => x.Value("type") == "GameValueContext"); 22 | 23 | public bool ApplyMigration(JObject obj, IList outMigrationReport) 24 | { 25 | bool didMigrate = false; 26 | foreach (var gameValueContextObj in GetGameValueContexts(obj)) 27 | { 28 | if (RenameProperty(gameValueContextObj, "contextName", "_name")) 29 | { 30 | outMigrationReport.Add($"Renamed 'contextName' to '_name' in '{gameValueContextObj.Value("type")}'"); 31 | didMigrate = true; 32 | } 33 | if (RenameProperty(gameValueContextObj, "titleBacking", "markedUpName")) 34 | { 35 | outMigrationReport.Add($"Renamed 'titleBacking' to 'markedUpName' in '{gameValueContextObj.Value("type")}'"); 36 | didMigrate = true; 37 | } 38 | if (RenameProperty(gameValueContextObj, "tooltip", "contextDescription")) 39 | { 40 | outMigrationReport.Add($"Renamed 'tooltip' to 'contextDescription' in '{gameValueContextObj.Value("type")}'"); 41 | didMigrate = true; 42 | } 43 | } 44 | return didMigrate; 45 | } 46 | 47 | private bool RenameProperty(JObject target, string oldKey, string newKey) 48 | { 49 | var oldValue = target.Value(oldKey); 50 | var newValue = target.Value(newKey); 51 | if (newValue == null && oldValue != null) 52 | { 53 | target[newKey] = oldValue; 54 | target.Remove(oldKey); 55 | return true; 56 | } 57 | return false; 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /EcoCivicsImportExportMod.Bundler/View/CivicBundleView.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using System.Windows; 7 | using System.Windows.Controls; 8 | using System.Windows.Data; 9 | using System.Windows.Documents; 10 | using System.Windows.Input; 11 | using System.Windows.Media; 12 | using System.Windows.Media.Imaging; 13 | using System.Windows.Navigation; 14 | using System.Windows.Shapes; 15 | 16 | namespace EcoCivicsImportExportMod.Bundler.View 17 | { 18 | /// 19 | /// Interaction logic for CivicBundleView.xaml 20 | /// 21 | public partial class CivicBundleView : UserControl 22 | { 23 | public CivicBundleView() 24 | { 25 | InitializeComponent(); 26 | } 27 | 28 | private void TreeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs e) 29 | { 30 | var civicBundle = DataContext as ViewModel.CivicBundle; 31 | if (civicBundle != null && e.NewValue is ViewModel.CivicObject civicObject) 32 | { 33 | civicBundle.SelectedCivicObject = civicObject; 34 | } 35 | else 36 | { 37 | civicBundle.SelectedCivicObject = null; 38 | } 39 | } 40 | 41 | private void TreeView_Drop(object sender, DragEventArgs e) 42 | { 43 | var bundle = DataContext as ViewModel.CivicBundle; 44 | if (bundle == null) { return; } 45 | bundle.IncomingDrop = false; 46 | var data = e.Data as DataObject; 47 | if (data == null) { return; } 48 | if (!data.ContainsFileDropList()) { return; } 49 | var fileDropList = data.GetFileDropList(); 50 | string[] arr = new string[fileDropList.Count]; 51 | fileDropList.CopyTo(arr, 0); 52 | if (BundlerCommands.AddToBundle.CanExecute(arr, this)) 53 | { 54 | BundlerCommands.AddToBundle.Execute(arr, this); 55 | } 56 | } 57 | 58 | private void TreeView_DragEnter(object sender, DragEventArgs e) 59 | { 60 | var bundle = DataContext as ViewModel.CivicBundle; 61 | var data = e.Data as DataObject; 62 | if (bundle == null || data == null) { return; } 63 | if (!data.ContainsFileDropList()) { return; } 64 | var fileDropList = data.GetFileDropList(); 65 | string[] arr = new string[fileDropList.Count]; 66 | fileDropList.CopyTo(arr, 0); 67 | if (!BundlerCommands.AddToBundle.CanExecute(arr, this)) { return; } 68 | bundle.IncomingDrop = true; 69 | } 70 | 71 | private void TreeView_DragLeave(object sender, DragEventArgs e) 72 | { 73 | var bundle = DataContext as ViewModel.CivicBundle; 74 | if (bundle == null) { return; } 75 | bundle.IncomingDrop = false; 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: build for release 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | build-mod: 9 | name: Build Mod 10 | runs-on: ubuntu-latest 11 | env: 12 | ECO_BRANCH: staging 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Setup .NET Core 8.0 16 | uses: actions/setup-dotnet@v4 17 | with: 18 | dotnet-version: '8.0.x' 19 | - name: Fetch dependencies 20 | run: dotnet restore ./EcoCivicsImportExportMod/EcoCivicsImportExportMod.csproj 21 | env: 22 | DOTNET_SYSTEM_GLOBALIZATION_INVARIANT: 1 23 | - name: Build 24 | run: dotnet build ./EcoCivicsImportExportMod/EcoCivicsImportExportMod.csproj --configuration Release --no-restore /p:AssemblyVersion=${{github.event.release.tag_name}} 25 | env: 26 | DOTNET_SYSTEM_GLOBALIZATION_INVARIANT: 1 27 | - name: Upload build artifact 28 | uses: actions/upload-artifact@v4 29 | with: 30 | name: mod-binaries-${{github.event.release.tag_name}} 31 | path: EcoCivicsImportExportMod/bin/Release/net8.0/EcoCivicsImportExportMod.* 32 | build-bundler-tool: 33 | name: Build Bundler Tool 34 | runs-on: windows-latest 35 | steps: 36 | - uses: actions/checkout@v4 37 | - name: Setup .NET Core 5.0 38 | uses: actions/setup-dotnet@v4 39 | with: 40 | dotnet-version: '5.0.201' 41 | - name: Fetch dependencies 42 | run: dotnet restore ./EcoCivicsImportExportMod.Bundler/EcoCivicsImportExportMod.Bundler.csproj 43 | env: 44 | DOTNET_SYSTEM_GLOBALIZATION_INVARIANT: 1 45 | - name: Build 46 | run: dotnet build ./EcoCivicsImportExportMod.Bundler/EcoCivicsImportExportMod.Bundler.csproj --configuration Release --no-restore 47 | env: 48 | DOTNET_SYSTEM_GLOBALIZATION_INVARIANT: 1 49 | - name: Archive 50 | run: 7z a EcoCivicsImportExportMod.Bundler/bin/EcoCivicsImportExportMod.Bundler.zip ./EcoCivicsImportExportMod.Bundler/bin/Release/net5.0-windows/* 51 | - name: Upload build artifact 52 | uses: actions/upload-artifact@v4 53 | with: 54 | name: bundler-tool-binaries-${{github.event.release.tag_name}} 55 | path: EcoCivicsImportExportMod.Bundler/bin/EcoCivicsImportExportMod.Bundler.zip 56 | deploy: 57 | name: Upload Release Assets 58 | needs: 59 | - build-mod 60 | - build-bundler-tool 61 | runs-on: ubuntu-latest 62 | steps: 63 | - name: Download build artifact (mod) 64 | id: download-mod 65 | uses: actions/download-artifact@v4 66 | with: 67 | name: mod-binaries-${{github.event.release.tag_name}} 68 | - name: Upload release asset (mod) 69 | id: upload-release-asset-mod 70 | uses: actions/upload-release-asset@v1 71 | env: 72 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 73 | with: 74 | upload_url: ${{github.event.release.upload_url}} 75 | asset_path: ${{steps.download-mod.outputs.download-path}}/EcoCivicsImportExportMod.dll 76 | asset_name: EcoCivicsImportExportMod.dll 77 | asset_content_type: application/octet-stream 78 | - name: Download build artifact (bundler tool) 79 | id: download-bundler-tool 80 | uses: actions/download-artifact@v4 81 | with: 82 | name: bundler-tool-binaries-${{github.event.release.tag_name}} 83 | - name: Upload release asset (bundler tool) 84 | id: upload-release-asset-bundler-tool 85 | uses: actions/upload-release-asset@v1 86 | env: 87 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 88 | with: 89 | upload_url: ${{github.event.release.upload_url}} 90 | asset_path: ${{steps.download-bundler-tool.outputs.download-path}}/EcoCivicsImportExportMod.Bundler.zip 91 | asset_name: EcoCivicsImportExportMod.Bundler.zip 92 | asset_content_type: application/octet-stream 93 | -------------------------------------------------------------------------------- /EcoCivicsImportExportMod.Bundler/View/CivicBundleView.xaml: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /EcoCivicsImportExportMod/Migrations/0950/CivicArticle.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using System.Reflection; 7 | 8 | using Newtonsoft.Json.Linq; 9 | 10 | namespace Eco.Mods.CivicsImpExp.Migrations._0950 11 | { 12 | using Shared.Utils; 13 | 14 | public class CivicArticle : ICivicsImpExpMigratorV1 15 | { 16 | public bool ShouldMigrate(JObject obj) 17 | { 18 | if (obj.Value("type") != "Eco.Gameplay.Civics.Constitutional.ConstitutionalAmendment") { return false; } 19 | var newArticles = obj.Value("properties").Value("NewArticles"); 20 | foreach (var article in newArticles) 21 | { 22 | var properties = article.Value("properties"); 23 | var executors = properties.Value("Executors"); 24 | if (executors == null || executors.Value("type") == "GameValueWrapper") { return true; } 25 | var proposers = properties.Value("Proposers"); 26 | if (proposers == null || proposers.Value("type") == "GameValueWrapper") { return true; } 27 | } 28 | return false; 29 | } 30 | 31 | private JObject CreateGamePickerList(string mustDeriveType, IEnumerable entries) 32 | { 33 | var gamePickerListObj = new JObject(); 34 | gamePickerListObj.Add("type", "GamePickerList"); 35 | var mustDeriveTypeObj = new JObject(); 36 | mustDeriveTypeObj.Add("type", "Type"); 37 | mustDeriveTypeObj.Add("value", mustDeriveType); 38 | gamePickerListObj.Add("mustDeriveType", mustDeriveTypeObj); 39 | gamePickerListObj.Add("requiredTag", null); 40 | gamePickerListObj.Add("internalDescription", "Any"); 41 | var entriesArr = new JArray(); 42 | foreach (var entry in entries) 43 | { 44 | entriesArr.Add(entry); 45 | } 46 | gamePickerListObj.Add("entries", entriesArr); 47 | return gamePickerListObj; 48 | } 49 | 50 | public bool ApplyMigration(JObject obj, IList outMigrationReport) 51 | { 52 | var newArticles = obj.Value("properties").Value("NewArticles"); 53 | bool didMigrate = false; 54 | foreach (var article in newArticles) 55 | { 56 | var properties = article.Value("properties"); 57 | var executors = properties.Value("Executors"); 58 | if (executors == null || executors.Value("type") == "GameValueWrapper") 59 | { 60 | var newExecutors = CreateGamePickerList("Eco.Gameplay.Alias.IAlias", executors != null ? new JObject[] 61 | { 62 | executors.Value("value") 63 | } : Enumerable.Empty()); 64 | properties.Remove("Executors"); 65 | properties.Add("Executors", newExecutors); 66 | outMigrationReport.Add($"Replaced a Executors {(executors == null ? "null" : "GameValueWrapper")} with a GameValuePicker in a civic article of '{obj.Value("name")}'"); 67 | didMigrate = true; 68 | } 69 | var proposers = properties.Value("Proposers"); 70 | if (proposers == null || proposers.Value("type") == "GameValueWrapper") 71 | { 72 | var newProposers = CreateGamePickerList("Eco.Gameplay.Alias.IAlias", proposers != null ? new JObject[] 73 | { 74 | proposers.Value("value") 75 | } : Enumerable.Empty()); 76 | properties.Remove("Proposers"); 77 | properties.Add("Proposers", newProposers); 78 | outMigrationReport.Add($"Replaced a Proposers {(proposers == null ? "null" : "GameValueWrapper")} with a GameValuePicker in a civic article of '{obj.Value("name")}'"); 79 | didMigrate = true; 80 | } 81 | } 82 | return didMigrate; 83 | } 84 | 85 | 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /EcoCivicsImportExportMod.Bundler/ViewModel/MainWindow.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using System.Windows; 4 | 5 | namespace EcoCivicsImportExportMod.Bundler.ViewModel 6 | { 7 | public class MainWindow : INotifyPropertyChanged 8 | { 9 | private bool incomingDrop; 10 | 11 | public Context Context { get; } 12 | 13 | public Visibility ShowUnloadedHint { get => Context.CivicBundle != null ? Visibility.Collapsed : Visibility.Visible; } 14 | 15 | public Visibility ShowLoadedView { get => Context.CivicBundle != null ? Visibility.Visible : Visibility.Collapsed; } 16 | 17 | public string Filename 18 | { 19 | get 20 | { 21 | if (Context.CivicBundle == null) { return ""; } 22 | if (string.IsNullOrEmpty(Context.FilePath)) { return "untitled*"; } 23 | return $"{System.IO.Path.GetFileName(Context.FilePath)}{(Context.LastSavePoint != 0 ? "*" : "")}"; 24 | } 25 | } 26 | 27 | public string WindowTitle 28 | { 29 | get 30 | { 31 | string filename = Filename; 32 | if (string.IsNullOrEmpty(filename)) 33 | { 34 | return "Eco Civics Bundler"; 35 | } 36 | return $"Eco Civics Bundler - {filename}"; 37 | } 38 | } 39 | 40 | private CivicBundle civicBundle; 41 | 42 | public CivicBundle CivicBundle 43 | { 44 | get => civicBundle; 45 | private set 46 | { 47 | if (value == civicBundle) { return; } 48 | civicBundle = value; 49 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CivicBundle))); 50 | } 51 | } 52 | 53 | public bool IncomingDrop 54 | { 55 | get => incomingDrop; 56 | set 57 | { 58 | if (value == incomingDrop) { return; } 59 | incomingDrop = value; 60 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IncomingDrop))); 61 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(DragTargetBorderSize))); 62 | } 63 | } 64 | 65 | public Thickness DragTargetBorderSize 66 | { 67 | get => incomingDrop ? new Thickness(3, 3, 3, 3) : new Thickness(0, 0, 0, 0); 68 | } 69 | 70 | public event PropertyChangedEventHandler PropertyChanged; 71 | 72 | public MainWindow(Context context) 73 | { 74 | Context = context; 75 | Context.OnCivicBundleChange += Context_OnCivicBundleChange; 76 | Context.OnFilePathChange += Context_OnFilePathChange; 77 | Context.OnLastSavePointChange += Context_OnLastSavePointChange; 78 | } 79 | 80 | private void Context_OnCivicBundleChange(object sender, EventArgs e) 81 | { 82 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ShowUnloadedHint))); 83 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ShowLoadedView))); 84 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Filename))); 85 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(WindowTitle))); 86 | if (Context.CivicBundle == null) 87 | { 88 | CivicBundle = null; 89 | } 90 | else if (CivicBundle == null) 91 | { 92 | CivicBundle = new CivicBundle(Context); 93 | } 94 | } 95 | 96 | private void Context_OnFilePathChange(object sender, EventArgs e) 97 | { 98 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Filename))); 99 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(WindowTitle))); 100 | } 101 | 102 | private void Context_OnLastSavePointChange(object sender, EventArgs e) 103 | { 104 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Filename))); 105 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(WindowTitle))); 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /EcoCivicsImportExportMod.Bundler/View/MainWindow.xaml: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /EcoCivicsImportExportMod.Bundler/ViewModel/CivicBundle.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.ObjectModel; 3 | using System.ComponentModel; 4 | using System.Linq; 5 | using System.Collections.Generic; 6 | using System.Windows; 7 | using System.Windows.Controls; 8 | 9 | namespace EcoCivicsImportExportMod.Bundler.ViewModel 10 | { 11 | public class CivicBundle : INotifyPropertyChanged 12 | { 13 | public static readonly IReadOnlyDictionary PreferredSortOrderForType = new Dictionary 14 | { 15 | { EcoTypes.Constitution, 0 }, 16 | { EcoTypes.ConstitutionalAmendment, 1 }, 17 | { EcoTypes.ElectionProcess, 2 }, 18 | { EcoTypes.Demographic, 3 }, 19 | { EcoTypes.DistrictMap, 4 }, 20 | { EcoTypes.ElectedTitle, 5 }, 21 | { EcoTypes.AppointedTitle, 6 }, 22 | { EcoTypes.Law, 7 } 23 | }; 24 | 25 | private CivicObject selectedCivicObject; 26 | private bool incomingDrop; 27 | 28 | public Context Context { get; } 29 | 30 | public Model.CivicBundle UnderlyingCivicBundle { get => Context.CivicBundle; } 31 | 32 | public string Name { get => string.IsNullOrEmpty(Context.FilePath) ? "Untitled Bundle" : System.IO.Path.GetFileNameWithoutExtension(Context.FilePath); } 33 | 34 | public ObservableCollection RootObjects { get; } = new ObservableCollection(); 35 | 36 | public ObservableCollection CivicObjects { get; } = new ObservableCollection(); 37 | 38 | public CivicObject SelectedCivicObject 39 | { 40 | get => selectedCivicObject; 41 | set 42 | { 43 | if (value == selectedCivicObject) { return; } 44 | selectedCivicObject = value; 45 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SelectedCivicObject))); 46 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ShowUnselectedHint))); 47 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ShowObjectDetails))); 48 | } 49 | } 50 | 51 | public Visibility ShowUnselectedHint 52 | { 53 | get => SelectedCivicObject != null ? Visibility.Collapsed : Visibility.Visible; 54 | } 55 | 56 | public Visibility ShowObjectDetails 57 | { 58 | get => SelectedCivicObject == null ? Visibility.Collapsed : Visibility.Visible; 59 | } 60 | 61 | public bool IncomingDrop 62 | { 63 | get => incomingDrop; 64 | set 65 | { 66 | if (value == incomingDrop) { return; } 67 | incomingDrop = value; 68 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IncomingDrop))); 69 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(TreeViewBorderSize))); 70 | } 71 | } 72 | 73 | public Thickness TreeViewBorderSize 74 | { 75 | get => incomingDrop ? new Thickness(3, 3, 3, 3) : new Thickness(0, 0, 0, 0); 76 | } 77 | 78 | public event PropertyChangedEventHandler PropertyChanged; 79 | 80 | public CivicBundle(Context context) 81 | { 82 | Context = context; 83 | RootObjects.Add(this); 84 | context.OnCivicBundleChange += Context_OnCivicBundleChange; 85 | context.OnFilePathChange += Context_OnFilePathChange; 86 | UpdateCivicObjects(); 87 | } 88 | 89 | private void Context_OnCivicBundleChange(object sender, EventArgs e) 90 | { 91 | UpdateCivicObjects(); 92 | } 93 | 94 | private void Context_OnFilePathChange(object sender, EventArgs e) 95 | { 96 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name))); 97 | } 98 | 99 | private IEnumerable SortCivics(IEnumerable bundledCivics) 100 | { 101 | return bundledCivics 102 | .GroupBy(c => c.Type) 103 | .OrderBy(g => PreferredSortOrderForType.TryGetValue(g.Key, out int sortOrder) ? sortOrder : int.MaxValue) 104 | .Select(g => g.OrderBy(c => c.Name)) 105 | .SelectMany(g => g); 106 | } 107 | 108 | private void UpdateCivicObjects() 109 | { 110 | if (UnderlyingCivicBundle == null) 111 | { 112 | CivicObjects.Clear(); 113 | return; 114 | } 115 | CivicObjects.SetFromEnumerable( 116 | SortCivics(UnderlyingCivicBundle.Civics), 117 | (in Model.BundledCivic bundledCivic) => new CivicObject(this, bundledCivic), 118 | (CivicObject viewModel, in Model.BundledCivic bundledCivic) => viewModel.BundledCivic = bundledCivic 119 | ); 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /EcoCivicsImportExportMod.Bundler/ViewModel/CivicObject.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using System.Collections.Generic; 4 | using System.Collections.ObjectModel; 5 | using System.Linq; 6 | 7 | namespace EcoCivicsImportExportMod.Bundler.ViewModel 8 | { 9 | public class CivicObject : INotifyPropertyChanged 10 | { 11 | private CivicBundle bundle; 12 | private Model.BundledCivic bundledCivic; 13 | 14 | public CivicBundle Bundle 15 | { 16 | get => bundle; 17 | set 18 | { 19 | if (value == bundle) { return; } 20 | bundle = value; 21 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Bundle))); 22 | } 23 | } 24 | 25 | public Model.BundledCivic BundledCivic 26 | { 27 | get => bundledCivic; 28 | set 29 | { 30 | bundledCivic = value; 31 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(BundledCivic))); 32 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name))); 33 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Description))); 34 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(RawJson))); 35 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IconSource))); 36 | UpdateSubobjects(); 37 | UpdateReferences(); 38 | 39 | } 40 | } 41 | 42 | public string Name 43 | { 44 | get => BundledCivic.Name; 45 | set 46 | { 47 | bundle.Context.RenameCivic(bundledCivic.AsReference, value); 48 | } 49 | } 50 | 51 | public string Description 52 | { 53 | get => BundledCivic.Data["description"]?.ToString() ?? string.Empty; 54 | set 55 | { 56 | bundle.Context.MutateBundledCivic(bundledCivic.AsReference, (in Model.BundledCivic bundledCivic) => 57 | { 58 | bundledCivic.Data["description"] = value; 59 | }); 60 | } 61 | } 62 | 63 | public string RawJson 64 | { 65 | get => bundledCivic.Data.ToString(); 66 | } 67 | 68 | public string IconSource 69 | { 70 | get => Icons.TypeToIconSource(bundledCivic.Type); 71 | } 72 | 73 | public ObservableCollection SubObjects { get; } = new ObservableCollection(); 74 | 75 | public ObservableCollection InternalReferences { get; } = new ObservableCollection(); 76 | 77 | public ObservableCollection ExternalReferences { get; } = new ObservableCollection(); 78 | 79 | public ObservableCollection InternalDependants { get; } = new ObservableCollection(); 80 | 81 | public event PropertyChangedEventHandler PropertyChanged; 82 | 83 | public CivicObject(CivicBundle bundle, Model.BundledCivic bundledCivic) 84 | { 85 | this.bundle = bundle; 86 | this.bundledCivic = bundledCivic; 87 | UpdateSubobjects(); 88 | UpdateReferences(); 89 | } 90 | 91 | private void UpdateSubobjects() 92 | { 93 | int i = 0; 94 | foreach (var inlineObject in BundledCivic.InlineObjects) 95 | { 96 | CivicObject civicObject; 97 | if (i >= SubObjects.Count) 98 | { 99 | civicObject = new CivicObject(bundle, inlineObject); 100 | SubObjects.Add(civicObject); 101 | } 102 | else 103 | { 104 | civicObject = SubObjects[i]; 105 | civicObject.BundledCivic = inlineObject; 106 | } 107 | ++i; 108 | } 109 | while (i < SubObjects.Count) 110 | { 111 | SubObjects.RemoveAt(i); 112 | } 113 | } 114 | 115 | private IEnumerable SortCivicReferences(IEnumerable civicReferences) 116 | { 117 | return civicReferences 118 | .GroupBy(c => c.Type) 119 | .OrderBy(g => CivicBundle.PreferredSortOrderForType.TryGetValue(g.Key, out int sortOrder) ? sortOrder : int.MaxValue) 120 | .Select(g => g.OrderBy(c => c.Name)) 121 | .SelectMany(g => g); 122 | } 123 | 124 | private void UpdateReferences() 125 | { 126 | InternalReferences.SetFromEnumerable( 127 | SortCivicReferences(bundledCivic.References.Where(r => bundle.UnderlyingCivicBundle.ReferenceIsLocal(r))), 128 | (in Model.CivicReference civicReference) => new CivicReference(civicReference), 129 | (CivicReference viewModel, in Model.CivicReference civicReference) => viewModel.UnderlyingCivicReference = civicReference 130 | ); 131 | ExternalReferences.SetFromEnumerable( 132 | SortCivicReferences(bundledCivic.References.Where(r => !bundle.UnderlyingCivicBundle.ReferenceIsLocal(r))), 133 | (in Model.CivicReference civicReference) => new CivicReference(civicReference), 134 | (CivicReference viewModel, in Model.CivicReference civicReference) => viewModel.UnderlyingCivicReference = civicReference 135 | ); 136 | InternalDependants.SetFromEnumerable( 137 | SortCivicReferences(bundle.UnderlyingCivicBundle.Civics.Where(c => c.References.Contains(bundledCivic.AsReference)).Select(c => c.AsReference)), 138 | (in Model.CivicReference civicReference) => new CivicReference(civicReference), 139 | (CivicReference viewModel, in Model.CivicReference civicReference) => viewModel.UnderlyingCivicReference = civicReference 140 | ); 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /EcoCivicsImportExportMod.Bundler/View/CivicObjectDetailView.xaml: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | 14 | 15 | 16 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | -------------------------------------------------------------------------------- /EcoCivicsImportExportMod.Bundler/Model/CivicBundle.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | using Newtonsoft.Json.Linq; 6 | 7 | namespace EcoCivicsImportExportMod.Bundler.Model 8 | { 9 | public delegate void NamedObjectVisitor(CivicReference civicReference, JObject obj); 10 | 11 | public readonly struct CivicReference : IEquatable 12 | { 13 | public readonly string Type; 14 | public readonly string Name; 15 | 16 | public CivicReference(string type, string name) 17 | { 18 | Type = type; 19 | Name = name; 20 | } 21 | 22 | public override bool Equals(object obj) 23 | => obj is CivicReference reference && Equals(reference); 24 | 25 | public bool Equals(CivicReference other) 26 | => Type == other.Type 27 | && Name == other.Name; 28 | 29 | public override int GetHashCode() 30 | => HashCode.Combine(Type, Name); 31 | 32 | public static bool operator ==(CivicReference left, CivicReference right) 33 | => left.Equals(right); 34 | 35 | public static bool operator !=(CivicReference left, CivicReference right) 36 | => !(left == right); 37 | 38 | public override string ToString() 39 | => $"{Type}:\"{Name}\""; 40 | } 41 | 42 | public readonly struct BundledCivic : ICloneable 43 | { 44 | public readonly JObject Data; 45 | 46 | public string Name { get => Data.Value("name"); } 47 | 48 | public string Type { get => Data.Value("type"); } 49 | 50 | public CivicReference AsReference { get => new CivicReference(Type, Name); } 51 | 52 | public IEnumerable References 53 | { 54 | get => FindNamedObjects(Data, true, true) 55 | .Select(t => t.Item1) 56 | .Distinct(); 57 | } 58 | 59 | public IEnumerable InlineObjects 60 | { 61 | get => FindNamedObjects(Data, false, true) 62 | .Select(t => new BundledCivic(t.Item2)); 63 | } 64 | 65 | public BundledCivic(JObject data) 66 | { 67 | Data = data; 68 | } 69 | 70 | public void VisitInlineObjects(NamedObjectVisitor visitor) 71 | => VisitNamedObjects(visitor, Data, false, true); 72 | 73 | public void VisitReferences(NamedObjectVisitor visitor) 74 | => VisitNamedObjects(visitor, Data, true, true); 75 | 76 | private static IEnumerable<(CivicReference CivicReference, JObject obj)> FindNamedObjects(JToken target, bool? references = null, bool ignoreRoot = false) 77 | { 78 | var result = new List<(CivicReference CivicReference, JObject Obj)>(); 79 | VisitNamedObjects((civicReference, obj) => result.Add((civicReference, obj)), target, references, ignoreRoot); 80 | return result; 81 | } 82 | 83 | private static void VisitNamedObjects(NamedObjectVisitor visitor, JToken target, bool? references = null, bool ignoreRoot = false) 84 | { 85 | if (target is JObject obj) 86 | { 87 | string name = obj.Value("name"); 88 | string typeName = obj.Value("type"); 89 | bool isRef = obj.Value("reference"); 90 | if (!ignoreRoot && !string.IsNullOrEmpty(name) && !string.IsNullOrEmpty(typeName)) 91 | { 92 | if (references == null || references.Value == isRef) 93 | { 94 | visitor(new CivicReference(typeName, name), obj); 95 | } 96 | return; 97 | } 98 | foreach (var pair in obj) 99 | { 100 | VisitNamedObjects(visitor, pair.Value, references); 101 | } 102 | } 103 | else if (target is JArray arr) 104 | { 105 | foreach (var element in arr) 106 | { 107 | VisitNamedObjects(visitor, element, references); 108 | } 109 | } 110 | } 111 | 112 | public object Clone() 113 | => new BundledCivic(Data.DeepClone() as JObject); 114 | } 115 | 116 | public class CivicBundle : ICloneable 117 | { 118 | public const int MajorVersion = 1; 119 | public const int MinorVersion = 0; 120 | 121 | #region Importing 122 | 123 | public static CivicBundle LoadFromText(string text) 124 | { 125 | JObject jsonObj = JObject.Parse(text); 126 | string typeName = jsonObj.Value("type"); 127 | var versionArr = jsonObj.Value("version"); 128 | int verMaj = versionArr.Value(0); 129 | //int verMin = versionArr.Value(1); 130 | if (verMaj != MajorVersion) 131 | { 132 | throw new InvalidOperationException($"Civic format not supported (found major '{verMaj}', expecting '{MajorVersion}'"); 133 | } 134 | if (typeName == "Eco.Mods.CivicsImpExp.CivicBundle") 135 | { 136 | // Importing formal bundle with multiple civics 137 | var civics = jsonObj.Value("civics"); 138 | var civicsList = new List(); 139 | for (int i = 0; i < civics.Count; ++i) 140 | { 141 | var element = civics.Value(i); 142 | civicsList.Add(new BundledCivic(element)); 143 | } 144 | return new CivicBundle(civicsList); 145 | } 146 | else 147 | { 148 | // Importing single civic 149 | return new CivicBundle(new BundledCivic[] { new BundledCivic(jsonObj) }); 150 | } 151 | } 152 | 153 | #endregion 154 | 155 | private readonly BundledCivic[] civics; 156 | 157 | public IEnumerable Civics { get => civics; } 158 | 159 | public IEnumerable AllReferences { get => Civics.SelectMany(c => c.References).Distinct(); } 160 | 161 | public IEnumerable AllInlineObjects { get => civics.SelectMany(c => c.InlineObjects); } 162 | 163 | public IEnumerable ExternalReferences { get => AllReferences.Where(r => !ReferenceIsLocal(r)); } 164 | 165 | public CivicBundle(IEnumerable civics = null) 166 | { 167 | this.civics = civics != null ? civics.ToArray() : new BundledCivic[0]; 168 | } 169 | 170 | public bool ReferenceIsLocal(CivicReference reference) 171 | => Civics.Select(c => c.AsReference).Contains(reference) 172 | || AllInlineObjects.Select(c => c.AsReference).Contains(reference); 173 | 174 | public object Clone() 175 | => new CivicBundle(Civics.Select(c => (BundledCivic)c.Clone())); 176 | 177 | public string SaveToText() 178 | { 179 | JObject jsonObj = new JObject(); 180 | jsonObj.Add("type", "Eco.Mods.CivicsImpExp.CivicBundle"); 181 | jsonObj.Add("version", new JArray(MajorVersion, MinorVersion)); 182 | JArray civicsArr = new JArray(); 183 | jsonObj.Add("civics", civicsArr); 184 | foreach (var civic in Civics) 185 | { 186 | civicsArr.Add(civic.Data); 187 | } 188 | return jsonObj.ToString(); 189 | } 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUnit 47 | *.VisualState.xml 48 | TestResult.xml 49 | nunit-*.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # StyleCop 65 | StyleCopReport.xml 66 | 67 | # Files built by Visual Studio 68 | *_i.c 69 | *_p.c 70 | *_h.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.iobj 75 | *.pch 76 | *.pdb 77 | *.ipdb 78 | *.pgc 79 | *.pgd 80 | *.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # TeamCity is a build add-in 131 | _TeamCity* 132 | 133 | # DotCover is a Code Coverage Tool 134 | *.dotCover 135 | 136 | # AxoCover is a Code Coverage Tool 137 | .axoCover/* 138 | !.axoCover/settings.json 139 | 140 | # Visual Studio code coverage results 141 | *.coverage 142 | *.coveragexml 143 | 144 | # NCrunch 145 | _NCrunch_* 146 | .*crunch*.local.xml 147 | nCrunchTemp_* 148 | 149 | # MightyMoose 150 | *.mm.* 151 | AutoTest.Net/ 152 | 153 | # Web workbench (sass) 154 | .sass-cache/ 155 | 156 | # Installshield output folder 157 | [Ee]xpress/ 158 | 159 | # DocProject is a documentation generator add-in 160 | DocProject/buildhelp/ 161 | DocProject/Help/*.HxT 162 | DocProject/Help/*.HxC 163 | DocProject/Help/*.hhc 164 | DocProject/Help/*.hhk 165 | DocProject/Help/*.hhp 166 | DocProject/Help/Html2 167 | DocProject/Help/html 168 | 169 | # Click-Once directory 170 | publish/ 171 | 172 | # Publish Web Output 173 | *.[Pp]ublish.xml 174 | *.azurePubxml 175 | # Note: Comment the next line if you want to checkin your web deploy settings, 176 | # but database connection strings (with potential passwords) will be unencrypted 177 | *.pubxml 178 | *.publishproj 179 | 180 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 181 | # checkin your Azure Web App publish settings, but sensitive information contained 182 | # in these scripts will be unencrypted 183 | PublishScripts/ 184 | 185 | # NuGet Packages 186 | *.nupkg 187 | # NuGet Symbol Packages 188 | *.snupkg 189 | # The packages folder can be ignored because of Package Restore 190 | **/[Pp]ackages/* 191 | # except build/, which is used as an MSBuild target. 192 | !**/[Pp]ackages/build/ 193 | # Uncomment if necessary however generally it will be regenerated when needed 194 | #!**/[Pp]ackages/repositories.config 195 | # NuGet v3's project.json files produces more ignorable files 196 | *.nuget.props 197 | *.nuget.targets 198 | 199 | # Microsoft Azure Build Output 200 | csx/ 201 | *.build.csdef 202 | 203 | # Microsoft Azure Emulator 204 | ecf/ 205 | rcf/ 206 | 207 | # Windows Store app package directories and files 208 | AppPackages/ 209 | BundleArtifacts/ 210 | Package.StoreAssociation.xml 211 | _pkginfo.txt 212 | *.appx 213 | *.appxbundle 214 | *.appxupload 215 | 216 | # Visual Studio cache files 217 | # files ending in .cache can be ignored 218 | *.[Cc]ache 219 | # but keep track of directories ending in .cache 220 | !?*.[Cc]ache/ 221 | 222 | # Others 223 | ClientBin/ 224 | ~$* 225 | *~ 226 | *.dbmdl 227 | *.dbproj.schemaview 228 | *.jfm 229 | *.pfx 230 | *.publishsettings 231 | orleans.codegen.cs 232 | 233 | # Including strong name files can present a security risk 234 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 235 | #*.snk 236 | 237 | # Since there are multiple workflows, uncomment next line to ignore bower_components 238 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 239 | #bower_components/ 240 | 241 | # RIA/Silverlight projects 242 | Generated_Code/ 243 | 244 | # Backup & report files from converting an old project file 245 | # to a newer Visual Studio version. Backup files are not needed, 246 | # because we have git ;-) 247 | _UpgradeReport_Files/ 248 | Backup*/ 249 | UpgradeLog*.XML 250 | UpgradeLog*.htm 251 | ServiceFabricBackup/ 252 | *.rptproj.bak 253 | 254 | # SQL Server files 255 | *.mdf 256 | *.ldf 257 | *.ndf 258 | 259 | # Business Intelligence projects 260 | *.rdl.data 261 | *.bim.layout 262 | *.bim_*.settings 263 | *.rptproj.rsuser 264 | *- [Bb]ackup.rdl 265 | *- [Bb]ackup ([0-9]).rdl 266 | *- [Bb]ackup ([0-9][0-9]).rdl 267 | 268 | # Microsoft Fakes 269 | FakesAssemblies/ 270 | 271 | # GhostDoc plugin setting file 272 | *.GhostDoc.xml 273 | 274 | # Node.js Tools for Visual Studio 275 | .ntvs_analysis.dat 276 | node_modules/ 277 | 278 | # Visual Studio 6 build log 279 | *.plg 280 | 281 | # Visual Studio 6 workspace options file 282 | *.opt 283 | 284 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 285 | *.vbw 286 | 287 | # Visual Studio LightSwitch build output 288 | **/*.HTMLClient/GeneratedArtifacts 289 | **/*.DesktopClient/GeneratedArtifacts 290 | **/*.DesktopClient/ModelManifest.xml 291 | **/*.Server/GeneratedArtifacts 292 | **/*.Server/ModelManifest.xml 293 | _Pvt_Extensions 294 | 295 | # Paket dependency manager 296 | .paket/paket.exe 297 | paket-files/ 298 | 299 | # FAKE - F# Make 300 | .fake/ 301 | 302 | # CodeRush personal settings 303 | .cr/personal 304 | 305 | # Python Tools for Visual Studio (PTVS) 306 | __pycache__/ 307 | *.pyc 308 | 309 | # Cake - Uncomment if you are using it 310 | # tools/** 311 | # !tools/packages.config 312 | 313 | # Tabs Studio 314 | *.tss 315 | 316 | # Telerik's JustMock configuration file 317 | *.jmconfig 318 | 319 | # BizTalk build output 320 | *.btp.cs 321 | *.btm.cs 322 | *.odx.cs 323 | *.xsd.cs 324 | 325 | # OpenCover UI analysis results 326 | OpenCover/ 327 | 328 | # Azure Stream Analytics local run output 329 | ASALocalRun/ 330 | 331 | # MSBuild Binary and Structured Log 332 | *.binlog 333 | 334 | # NVidia Nsight GPU debugger configuration file 335 | *.nvuser 336 | 337 | # MFractors (Xamarin productivity tool) working folder 338 | .mfractor/ 339 | 340 | # Local History for Visual Studio 341 | .localhistory/ 342 | 343 | # BeatPulse healthcheck temp database 344 | healthchecksdb 345 | 346 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 347 | MigrationBackup/ 348 | 349 | # Ionide (cross platform F# VS Code tools) working folder 350 | .ionide/ 351 | eco-dlls/ 352 | server-storage/ 353 | -------------------------------------------------------------------------------- /EcoCivicsImportExportMod.Bundler/View/MainWindow.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Windows; 3 | using System.Windows.Input; 4 | using System.Linq; 5 | 6 | using Microsoft.Win32; 7 | using System.Collections.Generic; 8 | 9 | namespace EcoCivicsImportExportMod.Bundler.View 10 | { 11 | /// 12 | /// Interaction logic for MainWindow.xaml 13 | /// 14 | public partial class MainWindow : Window 15 | { 16 | public Context Context { get; } 17 | 18 | public MainWindow() 19 | { 20 | InitializeComponent(); 21 | Context = new Context(); 22 | DataContext = new ViewModel.MainWindow(Context); 23 | } 24 | 25 | private bool CheckUnsavedChanges() 26 | { 27 | if (Context.LastSavePoint == 0) { return true; } 28 | var result = MessageBox.Show($"The current bundle has unsaved changes. Do you wish to save before proceeding?", "Eco Civic Bundler", MessageBoxButton.YesNoCancel, MessageBoxImage.Question); 29 | if (result == MessageBoxResult.No) { return true; } 30 | if (result == MessageBoxResult.Cancel) { return false; } 31 | if (string.IsNullOrEmpty(Context.FilePath)) 32 | { 33 | SaveFileDialog saveFileDialog = new SaveFileDialog(); 34 | saveFileDialog.Filter = "Json files (*.json)|*.json"; 35 | if (saveFileDialog.ShowDialog() != true) { return false; } 36 | Context.SaveAs(saveFileDialog.FileName); 37 | return true; 38 | } 39 | Context.Save(); 40 | return true; 41 | } 42 | 43 | #region Commands 44 | 45 | private void NewCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e) => e.CanExecute = true; 46 | 47 | private void NewCommand_Executed(object sender, ExecutedRoutedEventArgs e) 48 | { 49 | if (!CheckUnsavedChanges()) { return; } 50 | if (e.Parameter is IEnumerable newWith) 51 | { 52 | Context.NewWith(newWith); 53 | } 54 | else 55 | { 56 | Context.New(); 57 | } 58 | } 59 | 60 | private void OpenCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e) => e.CanExecute = true; 61 | 62 | private void OpenCommand_Executed(object sender, ExecutedRoutedEventArgs e) 63 | { 64 | if (!CheckUnsavedChanges()) { return; } 65 | OpenFileDialog openFileDialog = new OpenFileDialog(); 66 | openFileDialog.Filter = "Json files (*.json)|*.json"; 67 | openFileDialog.CheckFileExists = true; 68 | if (openFileDialog.ShowDialog() != true) { return; } 69 | Context.Load(openFileDialog.FileName); 70 | } 71 | 72 | private void SaveCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e) => e.CanExecute = Context.LastSavePoint != 0; 73 | 74 | private void SaveCommand_Executed(object sender, ExecutedRoutedEventArgs e) 75 | { 76 | if (string.IsNullOrEmpty(Context.FilePath)) 77 | { 78 | SaveAsCommand_Executed(sender, e); 79 | return; 80 | } 81 | Context.Save(); 82 | } 83 | 84 | private void SaveAsCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e) => e.CanExecute = true; 85 | 86 | private void SaveAsCommand_Executed(object sender, ExecutedRoutedEventArgs e) 87 | { 88 | SaveFileDialog saveFileDialog = new SaveFileDialog(); 89 | saveFileDialog.Filter = "Json files (*.json)|*.json"; 90 | if (saveFileDialog.ShowDialog() != true) { return; } 91 | Context.SaveAs(saveFileDialog.FileName); 92 | } 93 | 94 | private void CloseCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e) => e.CanExecute = true; 95 | 96 | private void CloseCommand_Executed(object sender, ExecutedRoutedEventArgs e) 97 | { 98 | if (!CheckUnsavedChanges()) { return; } 99 | Close(); 100 | } 101 | 102 | private void UndoCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e) => e.CanExecute = Context.CanUndo; 103 | 104 | private void UndoCommand_Executed(object sender, ExecutedRoutedEventArgs e) 105 | { 106 | Context.Undo(); 107 | } 108 | 109 | private void RedoCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e) => e.CanExecute = Context.CanRedo; 110 | 111 | private void RedoCommand_Executed(object sender, ExecutedRoutedEventArgs e) 112 | { 113 | Context.Redo(); 114 | } 115 | 116 | private void AddToBundleCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e) => e.CanExecute = Context.CivicBundle != null; 117 | 118 | private void AddToBundleCommand_Executed(object sender, ExecutedRoutedEventArgs e) 119 | { 120 | IEnumerable filePaths; 121 | if (e.Parameter is string str) 122 | { 123 | filePaths = new string[] { str }; 124 | } 125 | else if (e.Parameter is IEnumerable paramFilePaths) 126 | { 127 | filePaths = paramFilePaths; 128 | } 129 | else 130 | { 131 | OpenFileDialog openFileDialog = new OpenFileDialog(); 132 | openFileDialog.Title = "Add Civic to Bundle"; 133 | openFileDialog.Filter = "Json files (*.json)|*.json"; 134 | openFileDialog.CheckFileExists = true; 135 | openFileDialog.Multiselect = true; 136 | if (openFileDialog.ShowDialog() != true) { return; } 137 | filePaths = openFileDialog.FileNames; 138 | } 139 | Context.AddCivics(filePaths); 140 | } 141 | 142 | private void RemoveFromBundleCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e) 143 | { 144 | var civicObject = (e.Parameter as ViewModel.CivicObject) ?? (DataContext as ViewModel.MainWindow).CivicBundle?.SelectedCivicObject ?? null; 145 | if (civicObject == null) 146 | { 147 | e.CanExecute = false; 148 | return; 149 | } 150 | if (!Context.CivicBundle.Civics.Any(c => c.AsReference == civicObject.BundledCivic.AsReference)) 151 | { 152 | e.CanExecute = false; 153 | return; 154 | } 155 | e.CanExecute = true; 156 | } 157 | 158 | private void RemoveFromBundleCommand_Executed(object sender, ExecutedRoutedEventArgs e) 159 | { 160 | var civicObject = (e.Parameter as ViewModel.CivicObject) ?? (DataContext as ViewModel.MainWindow).CivicBundle?.SelectedCivicObject ?? null; 161 | if (civicObject == null) { return; } 162 | Context.RemoveCivic(civicObject.BundledCivic.AsReference); 163 | } 164 | 165 | #endregion 166 | 167 | private void Label_Drop(object sender, DragEventArgs e) 168 | { 169 | var mainWindow = DataContext as ViewModel.MainWindow; 170 | if (mainWindow == null) { return; } 171 | mainWindow.IncomingDrop = false; 172 | var data = e.Data as DataObject; 173 | if (data == null) { return; } 174 | if (!data.ContainsFileDropList()) { return; } 175 | var fileDropList = data.GetFileDropList(); 176 | string[] arr = new string[fileDropList.Count]; 177 | fileDropList.CopyTo(arr, 0); 178 | if (ApplicationCommands.New.CanExecute(arr, this)) 179 | { 180 | ApplicationCommands.New.Execute(arr, this); 181 | } 182 | } 183 | 184 | private void Label_DragEnter(object sender, DragEventArgs e) 185 | { 186 | var mainWindow = DataContext as ViewModel.MainWindow; 187 | var data = e.Data as DataObject; 188 | if (mainWindow == null || data == null) { return; } 189 | if (!data.ContainsFileDropList()) { return; } 190 | var fileDropList = data.GetFileDropList(); 191 | string[] arr = new string[fileDropList.Count]; 192 | fileDropList.CopyTo(arr, 0); 193 | if (!ApplicationCommands.New.CanExecute(arr, this)) { return; } 194 | mainWindow.IncomingDrop = true; 195 | } 196 | 197 | private void Label_DragLeave(object sender, DragEventArgs e) 198 | { 199 | var mainWindow = DataContext as ViewModel.MainWindow; 200 | if (mainWindow == null) { return; } 201 | mainWindow.IncomingDrop = false; 202 | } 203 | 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Eco Civics Import Export Mod 2 | A server mod for Eco 12.0 that allows admins to export the supported civics (listed below) from a server to json files, where they can be copied to another server and re-imported. 3 | 4 | Supported objects: 5 | - Laws 6 | - Election Processes 7 | - Elected Titles 8 | - Appointed (Registrar) Titles 9 | - Demographics 10 | - Constitutional Amendments 11 | - District Maps (only compatible when the world size is equivalent) 12 | - Government Bank Accounts 13 | 14 | ## Installation 15 | 1. Download `EcoCivicsImportExportMod.dll` from the [latest release](https://github.com/thomasfn/EcoCivicsImportExportMod/releases). 16 | 2. Copy the `EcoCivicsImportExportMod.dll` file to `Mods` folder of the dedicated server. 17 | 3. Restart the server. 18 | 19 | ## Usage 20 | 21 | All chat commands require admin privileges. 22 | 23 | ### Exporting Civics 24 | The `export` command will serialise the specific civic object to a json file in the server's working directory, under a folder called `civics`. 25 | 26 | `/civics export ` 27 | e.g. `/civics export 10` 28 | 29 | To find the ID of a civic, tag it in chat and hover it, the ID is displayed at the bottom of the popup. 30 | `civictype` must be one of: 31 | - `law` 32 | - `electionprocess` 33 | - `electedtitle` 34 | - `appointedtitle` 35 | - `demographic` 36 | - `constitution` 37 | - `amendment` 38 | - `districtmap` 39 | - `govaccount` 40 | - `settlement` 41 | - `immigrationpolicy` 42 | 43 | The filename of the exported file will be as follows (relative to the server's working directory): `civics/-.json`. The command will inform you of this filename if serialisation is successful. 44 | 45 | You can rename the file outside of the game if you wish, as the name of the file is specified in the import command. 46 | 47 | The `exportallof` command will serialise all civic objects of a type. It's the equivalent of running the `export` command for each civic object individually. 48 | 49 | `/civics exportallof [state]` 50 | e.g. `/civics exportallof law` or `/civics exportallof law,active` 51 | 52 | The `exportall` command will serialise all civic objects of all types. It's the equivalent of running the `exportallof` command for each civic type individually. 53 | 54 | `/civics exportall [state]` 55 | e.g. `/civics exportall` or `/civics exportall active` 56 | 57 | Note that the state argument in the above commands is optional, but if present must be one of: 58 | - `draft` (the civic has not yet been proposed) 59 | - `proposed` (the civic is proposed and an election is running for it) 60 | - `active` (the civic is in play) 61 | - `removed` (the civic has been entirely removed) 62 | 63 | For non-proposable objects, e.g. bank accounts or appointed titles, the state argument is ignored. 64 | 65 | ### Importing Civics 66 | The import command will attempt to deserialise a civic object from the specified json file. If it fails at any stage, the civic object (if it managed to created one) will be immediately destroyed with no side effects. The file must be placed in the "civics" folder in the server's working directory. Alternatively, a download URL may be specified. 67 | 68 | `/civics import ` 69 | e.g. `/civics import law-10.json` 70 | 71 | The civic object will be given draft status with the command executor as the owner, and put in the first available civic slot (e.g. a law will go to the first available Court). If there are no slots available, the command will fail. The executor of the command should have civic privileges to propose changes to civics of that type, or they may not be able to actually bring the imported civic to life. 72 | 73 | The civic object may have dependencies on other objects - for example, a law may reference a bank account or a district map, or an election process may reference a demographic. All dependencies must be present at the point of running the import command, or the command will fail. Dependencies are resolved via name - so if a law references a district called "Main Roads" and the server has a district called "Roads" instead, this will not work - either the district will need to be renamed for the dependency to be resolved, or the json file will need to be manually amended. Dependencies can be safely renamed after the import is complete. 74 | 75 | An import can be reversed by using the undo import command. 76 | 77 | `/civics undoimport` 78 | 79 | This will roll back the previously executed import, deleting any objects that it created. This will not go back more than one import, and it will not roll back imports from previous sessions (e.g. since a server restart). Use this command with caution as it wipes the objects entirely from memory without any regard for how they may have changed or been referenced since being imported. 80 | 81 | #### Government Accounts 82 | 83 | A bank account can be exported/imported as a civic if it's created as a Government Account. In this case it will persist the holdings (e.g. how much of each currency is held in the account), but not the transactions - the imported account will show an empty transaction log. The usual rules about dependencies apply to holdings as they reference currencies - these will need to be present before import. 84 | 85 | ### Importing Bundles 86 | A bundle is a number of civics that have been previously exported by the plugin, grouped up into a single file. This bundle can be imported via the same import command as single civics. When importing a bundle, all civics contained within the bundle are imported in one go, saving the need to run the command over and over again during workloads that involve importing a large number of civics (for example an entire government structure). Bundles can be assembled manually or using the included bundler tool (see [Bundler Tool](#bundler-tool)). 87 | 88 | `/civics import ` 89 | e.g. `/civics import my-bundle.json` 90 | 91 | A bundle can only be imported if all dependencies of that bundle are present beforehand. The plugin will not import _any_ civics from the bundle if some references can't be resolved. The bundle info command will print details about the bundle, including any dependencies and whether or not they could be resolved, without actually attempting an import - e.g. it is safe to run with no side effects. 92 | 93 | `/civics bundleinfo ` 94 | e.g. `/civics bundleinfo my-bundle.json` 95 | 96 | The bundle info command will work on single civics too, as they are just considered a bundle with one civic contained within. 97 | 98 | ### Bundler Tool 99 | The bundler tool allows civics that have been previously exported by the plugin to be grouped together into a single bundle, ready to be imported by the plugin in one go. It has a simple UI and only works on Windows. 100 | 101 | #### Installation 102 | 1. Download the tool from the latest release 103 | 2. Extract the zip to a location of your choosing 104 | 3. Run the executable `EcoCivicsImportExportMod.Bundler.exe` 105 | 106 | #### Usage 107 | When you first open the tool, you'll be presented with a clean slate. 108 | 109 | ![Bundler Clean Slate](./screenshots/bundler-fresh.png "Bundler Clean Slate") 110 | 111 | As the tip suggests, there are multiple ways you can get started. You can open an existing bundle or create a new one via the File menu, or drag one or more civic json files onto the tool from Windows Explorer. If you choose to create a new bundle, you'll be presented with an empty untitled bundle. 112 | 113 | ![Bundler New Bundle](./screenshots/bundler-new.png "Bundler New Bundle") 114 | 115 | In this view you can drag one or more civic json files onto the tool from Windows Explorer, or select Add to Bundle from the Edit menu to add civics to the bundle. 116 | 117 | ![Bundler Imported Civics](./screenshots/bundler-imported.png "Bundler Imported Civics") 118 | 119 | As you add civics to the bundle, they will display on the tree view to the left, by order of type and then name. The icons can be used to tell the civic types apart at a glance. Some civics hold sub-objects, for example a District Map may hold multiple Districts, and these can be seen by expanding the civic object node in the tree view. You can right click civics and click Remove from Bundle, or select them and click Remove Selected from Bundle in the Edit menu to remove a civic from the bundle. You can select any civic object node from the tree view to view further details. 120 | 121 | ![Bundler Object Detail View](./screenshots/bundler-detailview.png "Object Detail View") 122 | 123 | The detail view displays key information about the civic, including any references it has to other civics (either 'Internal', that is within the bundle, or 'External', that is outside of the bundle) and any other civics within the bundle that reference the civic ('Dependants'). The name and description of the civic objects can be changed here, but all other properties are immutable. 124 | 125 | Once the bundle has been assembled to your satisfaction, simply save it to the server's `civics` folder and the plugin's import command should be able to import it. 126 | 127 | ## Building Mod from Source 128 | 129 | ### Windows 130 | 131 | 1. Open `EcoCivicsImportExportMod.sln` in Visual Studio 2019/2022 132 | 2. Build the `EcoCivicsImportExportMod` project in Visual Studio 133 | 3. Find the artifact in `EcoCivicsImportExportMod\bin\{Debug|Release}\net8.0` 134 | 135 | ### Linux 136 | 1. Enter the `EcoCivicsImportExportMod` directory and run: 137 | `dotnet restore` 138 | `dotnet build` 139 | 2. Find the artifact in `EcoCivicsImportExportMod/bin/{Debug|Release}/net8.0` 140 | 141 | ## Building Bundler Tool from Source 142 | 143 | ### Windows 144 | 145 | 1. Open `EcoCivicsImportExportMod.sln` in Visual Studio 2019/2022 146 | 2. Build the `EcoCivicsImportExportMod.Bundler` project in Visual Studio 147 | 3. Find the artifact in `EcoCivicsImportExportMod.Bundler\bin\{Debug|Release}\net5.0-windows` 148 | 149 | ## Attributions 150 | - Some icons used in the Bundler Tool are by [Yusuke Kamiyamane](http://p.yusukekamiyamane.com/) licensed under a [Creative Commons Attribution 3.0 License](http://creativecommons.org/licenses/by/3.0/). 151 | 152 | ## License 153 | [MIT](https://choosealicense.com/licenses/mit/) -------------------------------------------------------------------------------- /EcoCivicsImportExportMod.Bundler/Context.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.IO; 5 | 6 | namespace EcoCivicsImportExportMod.Bundler 7 | { 8 | using Model; 9 | using System.Windows; 10 | 11 | public delegate CivicBundle CivicBundleMutator(CivicBundle oldCivicBundle); 12 | public delegate void BundledCivicMutator(in BundledCivic bundledCivic); 13 | 14 | public class Context 15 | { 16 | private readonly Stack undoStack = new Stack(); 17 | private readonly Stack redoStack = new Stack(); 18 | 19 | private CivicBundle civicBundle; 20 | private string filePath; 21 | private int lastSavePoint; 22 | 23 | public CivicBundle CivicBundle 24 | { 25 | get => civicBundle; 26 | private set 27 | { 28 | if (value == civicBundle) { return; } 29 | civicBundle = value; 30 | OnCivicBundleChange?.Invoke(this, EventArgs.Empty); 31 | } 32 | } 33 | 34 | public string FilePath 35 | { 36 | get => filePath; 37 | private set 38 | { 39 | if (value == filePath) { return; } 40 | filePath = value; 41 | OnFilePathChange?.Invoke(this, EventArgs.Empty); 42 | } 43 | } 44 | 45 | public int LastSavePoint 46 | { 47 | get => lastSavePoint; 48 | private set 49 | { 50 | if (value == lastSavePoint) { return; } 51 | lastSavePoint = value; 52 | OnLastSavePointChange?.Invoke(this, EventArgs.Empty); 53 | } 54 | } 55 | 56 | public bool CanUndo => undoStack.Count > 0; 57 | 58 | public bool CanRedo => redoStack.Count > 0; 59 | 60 | public event EventHandler OnCivicBundleChange; 61 | public event EventHandler OnFilePathChange; 62 | public event EventHandler OnLastSavePointChange; 63 | 64 | public void New() 65 | { 66 | CivicBundle = new CivicBundle(); 67 | FilePath = null; 68 | undoStack.Clear(); 69 | redoStack.Clear(); 70 | LastSavePoint = 1; 71 | } 72 | 73 | public void NewWith(IEnumerable filePaths) 74 | { 75 | if (filePaths.Count() == 1) 76 | { 77 | Load(filePaths.Single()); 78 | return; 79 | } 80 | CivicBundle = new CivicBundle(); 81 | FilePath = null; 82 | AddCivics(filePaths); 83 | undoStack.Clear(); 84 | redoStack.Clear(); 85 | LastSavePoint = 1; 86 | } 87 | 88 | private CivicBundle OpenBundle(string filename) 89 | { 90 | string text; 91 | try 92 | { 93 | text = File.ReadAllText(filename); 94 | } 95 | catch (Exception ex) 96 | { 97 | MessageBox.Show($"Failed to load '{Path.GetFileName(filePath)}' ({ex.Message})!", "Eco Civic Bundler", MessageBoxButton.OK, MessageBoxImage.Error); 98 | return null; 99 | } 100 | try 101 | { 102 | return CivicBundle.LoadFromText(text); 103 | } 104 | catch (Exception ex) 105 | { 106 | MessageBox.Show($"Failed to parse bundle from '{Path.GetFileName(filePath)}' ({ex.Message})!", "Eco Civic Bundler", MessageBoxButton.OK, MessageBoxImage.Error); 107 | return null; 108 | } 109 | } 110 | 111 | public void Load(string filePath) 112 | { 113 | var bundle = OpenBundle(filePath); 114 | if (bundle == null) { return; } 115 | CivicBundle = bundle; 116 | FilePath = filePath; 117 | undoStack.Clear(); 118 | redoStack.Clear(); 119 | LastSavePoint = 0; 120 | } 121 | 122 | public void Mutate(CivicBundleMutator mutator) 123 | { 124 | if (civicBundle == null) { return; } 125 | var oldCivicBundle = civicBundle; 126 | var newCivicBundle = mutator(oldCivicBundle); 127 | CivicBundle = newCivicBundle; 128 | undoStack.Push(oldCivicBundle); 129 | redoStack.Clear(); 130 | ++LastSavePoint; 131 | } 132 | 133 | public void MutateBundledCivic(CivicReference civicReference, BundledCivicMutator mutator) 134 | { 135 | Mutate((oldCivicBundle) => 136 | { 137 | var civics = oldCivicBundle.Civics.Select(c => (BundledCivic)c.Clone()).ToArray(); 138 | for (int i = 0, l = civics.Length; i < l; ++i) 139 | { 140 | ref BundledCivic civic = ref civics[i]; 141 | if (civic.AsReference == civicReference) 142 | { 143 | mutator(civic); 144 | break; 145 | } 146 | foreach (var inlineObject in civic.InlineObjects) 147 | { 148 | if (inlineObject.AsReference == civicReference) 149 | { 150 | mutator(inlineObject); 151 | break; 152 | } 153 | } 154 | } 155 | return new CivicBundle(civics); 156 | }); 157 | } 158 | 159 | public void RenameCivic(CivicReference civicReference, string newName) 160 | { 161 | if (civicBundle == null) { return; } 162 | BundledCivic? civicToRename = FindCivic(civicReference); 163 | if (civicToRename == null) 164 | { 165 | MessageBox.Show($"Failed to rename civic - not found in this bundle!", "Eco Civic Bundler", MessageBoxButton.OK, MessageBoxImage.Error); 166 | return; 167 | } 168 | CivicReference renamedCivicReference = new CivicReference(civicReference.Type, newName); 169 | BundledCivic? existingCivic = FindCivic(renamedCivicReference); 170 | if (existingCivic != null) 171 | { 172 | MessageBox.Show($"Failed to rename civic - another civic by that name already exists in this bundle!", "Eco Civic Bundler", MessageBoxButton.OK, MessageBoxImage.Error); 173 | return; 174 | } 175 | int renameCnt = 0, fixupCnt = 0; 176 | Mutate((oldCivicBundle) => 177 | { 178 | var civics = oldCivicBundle.Civics.Select(c => (BundledCivic)c.Clone()).ToArray(); 179 | for (int i = 0, l = civics.Length; i < l; ++i) 180 | { 181 | ref BundledCivic civic = ref civics[i]; 182 | if (civic.AsReference == civicReference) 183 | { 184 | civic.Data["name"] = newName; 185 | ++renameCnt; 186 | } 187 | else 188 | { 189 | civic.VisitInlineObjects((inlineCivicReference, obj) => 190 | { 191 | if (inlineCivicReference == civicReference) 192 | { 193 | obj["name"] = newName; 194 | ++renameCnt; 195 | } 196 | }); 197 | } 198 | civic.VisitReferences((inlineCivicReference, obj) => 199 | { 200 | if (inlineCivicReference == civicReference) 201 | { 202 | obj["name"] = newName; 203 | ++fixupCnt; 204 | } 205 | }); 206 | } 207 | return new CivicBundle(civics); 208 | }); 209 | if (renameCnt != 1) 210 | { 211 | MessageBox.Show($"Failed to rename civic - renameCnt was {renameCnt}, expecting 1!", "Eco Civic Bundler", MessageBoxButton.OK, MessageBoxImage.Error); 212 | return; 213 | } 214 | if (fixupCnt > 0) 215 | { 216 | MessageBox.Show($"Renamed '{civicReference.Name}' to '{newName}' and fixed up {fixupCnt} internal references.", "Eco Civic Bundler", MessageBoxButton.OK, MessageBoxImage.Information); 217 | } 218 | else 219 | { 220 | MessageBox.Show($"Renamed '{civicReference.Name}' to '{newName}'.", "Eco Civic Bundler", MessageBoxButton.OK, MessageBoxImage.Information); 221 | } 222 | } 223 | 224 | public void AddCivics(IEnumerable filePaths) 225 | { 226 | Mutate((oldCivicBundle) => 227 | { 228 | var civics = new List(oldCivicBundle.Civics.Select(c => (BundledCivic)c.Clone())); 229 | foreach (string filePath in filePaths) 230 | { 231 | var bundle = OpenBundle(filePath); 232 | if (bundle == null) { continue; } 233 | foreach (var innerCivic in bundle.Civics) 234 | { 235 | if (FindCivic(innerCivic.AsReference) != null) 236 | { 237 | MessageBox.Show($"Failed to add civic '{innerCivic.Name}' - a civic by that name already exists in this bundle!", "Eco Civic Bundler", MessageBoxButton.OK, MessageBoxImage.Error); 238 | continue; 239 | } 240 | var conflicts = innerCivic.InlineObjects.Where(c => FindCivic(c.AsReference) != null); 241 | if (conflicts.Any()) 242 | { 243 | MessageBox.Show($"Failed to add civic '{innerCivic.Name}' - this civic contains the following inline objects that already exist in this bundle:\n{string.Join("\n", conflicts.Select(c => $" - '{c.Name}"))}", "Eco Civic Bundler", MessageBoxButton.OK, MessageBoxImage.Error); 244 | continue; 245 | } 246 | civics.Add(innerCivic); 247 | } 248 | } 249 | return new CivicBundle(civics); 250 | }); 251 | } 252 | 253 | public void RemoveCivic(CivicReference civicReference) 254 | { 255 | // TODO: Add support for removing inline objects (e.g. a district from a district map) 256 | Mutate(civicBundle => 257 | new CivicBundle( 258 | civicBundle.Civics 259 | .Except(civicBundle.Civics.Where(c => c.AsReference == civicReference)) 260 | .Select(c => (BundledCivic)c.Clone()) 261 | ) 262 | ); 263 | } 264 | 265 | private BundledCivic? FindCivic(CivicReference civicReference) 266 | { 267 | foreach (var civic in civicBundle.Civics) 268 | { 269 | if (civic.AsReference == civicReference) 270 | { 271 | return civic; 272 | } 273 | } 274 | foreach (var civic in civicBundle.AllInlineObjects) 275 | { 276 | if (civic.AsReference == civicReference) 277 | { 278 | return civic; 279 | } 280 | } 281 | return null; 282 | } 283 | 284 | public bool Undo() 285 | { 286 | if (undoStack.Count == 0) { return false; } 287 | if (civicBundle != null) 288 | { 289 | redoStack.Push(civicBundle); 290 | } 291 | civicBundle = undoStack.Pop(); 292 | OnCivicBundleChange?.Invoke(this, EventArgs.Empty); 293 | --LastSavePoint; 294 | return true; 295 | } 296 | 297 | public bool Redo() 298 | { 299 | if (redoStack.Count == 0) { return false; } 300 | if (civicBundle != null) 301 | { 302 | undoStack.Push(civicBundle); 303 | } 304 | civicBundle = redoStack.Pop(); 305 | OnCivicBundleChange?.Invoke(this, EventArgs.Empty); 306 | ++LastSavePoint; 307 | return true; 308 | } 309 | 310 | public void Save() 311 | { 312 | if (string.IsNullOrEmpty(filePath) || civicBundle == null) { return; } 313 | System.IO.File.WriteAllText(filePath, civicBundle.SaveToText()); 314 | LastSavePoint = 0; 315 | } 316 | 317 | public void SaveAs(string filePath) 318 | { 319 | FilePath = filePath; 320 | Save(); 321 | } 322 | } 323 | } 324 | -------------------------------------------------------------------------------- /EcoCivicsImportExportMod/CivicsJsonConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Linq; 4 | using System.Reflection; 5 | 6 | using Newtonsoft.Json; 7 | using Newtonsoft.Json.Linq; 8 | 9 | namespace Eco.Mods.CivicsImpExp 10 | { 11 | using Core.Systems; 12 | 13 | using Shared.Networking; 14 | using Shared.Localization; 15 | using Shared.Math; 16 | using Shared.Utils; 17 | 18 | using Gameplay.LegislationSystem; 19 | using Gameplay.Civics.Misc; 20 | using Gameplay.Civics.GameValues; 21 | using Gameplay.Civics.Districts; 22 | using Gameplay.GameActions; 23 | using Gameplay.Utils; 24 | using Gameplay.Economy.Money; 25 | using Gameplay.Economy; 26 | 27 | public class CivicsJsonConverter : JsonConverter 28 | { 29 | /// 30 | /// Serialised civics authored with a different major version are completely incompatible. 31 | /// 32 | public const int MajorVersion = 1; 33 | 34 | /// 35 | /// Serialised civics authored with a different minor version are compatible. 36 | /// 37 | public const int MinorVersion = 0; 38 | 39 | #region Serialisation 40 | 41 | public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) 42 | { 43 | JObject rootObj; 44 | if (value is DistrictMap districtMap) 45 | { 46 | rootObj = SerialiseDistrictMap(districtMap); 47 | } 48 | else if (value is BankAccount bankAccount) 49 | { 50 | rootObj = SerialiseBankAccount(bankAccount); 51 | } 52 | else 53 | { 54 | rootObj = SerialiseGenericObject(value, value as IHasSubRegistrarEntries); 55 | } 56 | rootObj.AddFirst(new JProperty("version", new int[] { MajorVersion, MinorVersion })); 57 | rootObj.WriteTo(writer); 58 | } 59 | 60 | private JObject SerialiseGenericObject(object value, IHasSubRegistrarEntries inlineObjectContext = null) 61 | { 62 | var obj = new JObject(); 63 | obj.Add(new JProperty("type", value.GetType().FullName)); 64 | if (value is INamed named) 65 | { 66 | obj.Add(new JProperty("name", SerialiseValue(named.Name))); 67 | } 68 | obj.Add(new JProperty("reference", false)); 69 | if (value is SimpleEntry simpleEntry) 70 | { 71 | obj.Add(new JProperty("description", SerialiseValue(simpleEntry.UserDescription))); 72 | } 73 | if (value is IHasDualPermissions hasDualPermissions) 74 | { 75 | obj.Add(new JProperty("managers", SerialiseValue(hasDualPermissions.DualPermissions.ManagerSet))); 76 | obj.Add(new JProperty("users", SerialiseValue(hasDualPermissions.DualPermissions.UserSet))); 77 | } 78 | obj.Add(new JProperty("properties", SerialiseObjectProperties(value, value as IHasSubRegistrarEntries))); 79 | return obj; 80 | } 81 | 82 | private JObject SerialiseObjectProperties(object value, IHasSubRegistrarEntries inlineObjectContext = null) 83 | { 84 | var obj = new JObject(); 85 | 86 | var properties = value.GetType() 87 | .GetProperties() 88 | .Where((propInfo) => propInfo.GetCustomAttribute() != null); 89 | 90 | foreach (var propInfo in properties) 91 | { 92 | var token = SerialiseValue(propInfo.GetValue(value), inlineObjectContext); 93 | obj.Add(new JProperty(propInfo.Name, token)); 94 | } 95 | 96 | return obj; 97 | } 98 | 99 | private object SerialiseValue(object value, IHasSubRegistrarEntries inlineObjectContext = null) 100 | { 101 | if (value == null) 102 | { 103 | return null; 104 | } 105 | else if (value is int intValue) 106 | { 107 | return intValue; 108 | } 109 | else if (value is bool boolValue) 110 | { 111 | return boolValue; 112 | } 113 | else if (value is float floatValue) 114 | { 115 | return floatValue; 116 | } 117 | else if (value is double doubleValue) 118 | { 119 | return doubleValue; 120 | } 121 | else if (value is string stringValue) 122 | { 123 | return stringValue; 124 | } 125 | else if (value is Color color) 126 | { 127 | var jsonArr = new JArray(); 128 | jsonArr.Add(color.R); 129 | jsonArr.Add(color.G); 130 | jsonArr.Add(color.B); 131 | jsonArr.Add(color.A); 132 | return jsonArr; 133 | } 134 | else if (value is LocString locStringValue) 135 | { 136 | return locStringValue.ToString(); 137 | } 138 | else if (value is Type typeValue) 139 | { 140 | var jsonObj = new JObject(); 141 | jsonObj.Add(new JProperty("type", "Type")); 142 | jsonObj.Add(new JProperty("value", typeValue.FullName)); 143 | return jsonObj; 144 | } 145 | else if (value is GamePickerList gamePickerListValue) 146 | { 147 | return SerialiseGamePickerList(gamePickerListValue, inlineObjectContext); 148 | } 149 | else if (value is IGameValueContext gameValueContext) 150 | { 151 | return SerialiseGameValueContext(gameValueContext); 152 | } 153 | else if (value is INamed namedValue && (inlineObjectContext == null || !inlineObjectContext.SubRegistrarEntries.Contains(namedValue))) 154 | { 155 | return SerialiseObjectReference(namedValue); 156 | } 157 | else if (value is IEnumerable enumerableValue) 158 | { 159 | return SerialiseList(enumerableValue, inlineObjectContext); 160 | } 161 | else if (value is TriggerConfig triggerConfig) 162 | { 163 | return SerialiseTriggerConfig(triggerConfig); 164 | } 165 | else if (value is GameValue gameValue) 166 | { 167 | return SerialiseGameValue(gameValue); 168 | } 169 | else if (value is DistrictMap districtMap) 170 | { 171 | return SerialiseDistrictMap(districtMap); 172 | } 173 | else if (value is District district) 174 | { 175 | return SerialiseDistrict(district); 176 | } 177 | else if (value.GetType().IsEnum) 178 | { 179 | return value.GetType().GetEnumName(value); 180 | } 181 | else if (value.GetType().IsClass) 182 | { 183 | return SerialiseGenericObject(value, inlineObjectContext); 184 | } 185 | else 186 | { 187 | var jsonObj = new JObject(); 188 | jsonObj.Add(new JProperty("type", "unknown")); 189 | jsonObj.Add(new JProperty("actualType", value.GetType().Name)); 190 | jsonObj.Add(new JProperty("info", value.ToString())); 191 | return jsonObj; 192 | } 193 | } 194 | 195 | private JObject SerialiseObjectReference(INamed value) 196 | { 197 | var jsonObj = new JObject(); 198 | jsonObj.Add(new JProperty("type", value.GetType().FullName)); 199 | jsonObj.Add(new JProperty("name", value.Name)); 200 | jsonObj.Add(new JProperty("reference", true)); 201 | return jsonObj; 202 | } 203 | 204 | private JArray SerialiseList(IEnumerable enumerableValue, IHasSubRegistrarEntries inlineObjectContext) 205 | { 206 | var jsonArr = new JArray(); 207 | foreach (object value in enumerableValue) 208 | { 209 | jsonArr.Add(SerialiseValue(value, inlineObjectContext)); 210 | } 211 | return jsonArr; 212 | } 213 | 214 | private JObject SerialiseTriggerConfig(TriggerConfig triggerConfig) 215 | { 216 | var jsonObj = new JObject(); 217 | jsonObj.Add(new JProperty("type", triggerConfig.GetType().FullName)); 218 | var typeToConfig = triggerConfig.GetType().GetProperty("TypeToConfig", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(triggerConfig) as Type; 219 | jsonObj.Add(new JProperty("typeToConfig", SerialiseValue(typeToConfig?.FullName))); 220 | jsonObj.Add(new JProperty("propNameBacker", SerialiseValue(triggerConfig.PropNameBacker))); 221 | var propDisplayName = typeof(TriggerConfig).GetProperty("PropDisplayName", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(triggerConfig) as string; 222 | jsonObj.Add(new JProperty("propDisplayName", SerialiseValue(propDisplayName))); 223 | jsonObj.Add(new JProperty("properties", SerialiseObjectProperties(triggerConfig))); 224 | return jsonObj; 225 | } 226 | 227 | private JObject SerialiseGamePickerList(GamePickerList gamePickerListValue, IHasSubRegistrarEntries inlineObjectContext) 228 | { 229 | var jsonObj = new JObject(); 230 | jsonObj.Add(new JProperty("type", "GamePickerList")); 231 | jsonObj.Add(new JProperty("mustDeriveType", SerialiseValue(gamePickerListValue.MustDeriveType, inlineObjectContext))); 232 | jsonObj.Add(new JProperty("requiredTag", SerialiseValue(gamePickerListValue.RequiredTag, inlineObjectContext))); 233 | jsonObj.Add(new JProperty("internalDescription", SerialiseValue(gamePickerListValue.InternalDescription, inlineObjectContext))); 234 | jsonObj.Add(new JProperty("entries", SerialiseList(gamePickerListValue.Entries, inlineObjectContext))); 235 | return jsonObj; 236 | } 237 | 238 | private JObject SerialiseGameValue(GameValue gameValue) 239 | { 240 | var jsonObj = new JObject(); 241 | if (gameValue.GetType().IsConstructedGenericType && gameValue.GetType().GetGenericTypeDefinition() == typeof(GameValueWrapper<>)) 242 | { 243 | jsonObj.Add(new JProperty("type", "GameValueWrapper")); 244 | object wrappedValue = gameValue.GetType().GetProperty("Object", BindingFlags.Public | BindingFlags.Instance).GetValue(gameValue); 245 | jsonObj.Add(new JProperty("value", SerialiseValue(wrappedValue))); 246 | } 247 | else 248 | { 249 | jsonObj.Add(new JProperty("type", gameValue.GetType().FullName)); 250 | jsonObj.Add(new JProperty("properties", SerialiseObjectProperties(gameValue))); 251 | } 252 | return jsonObj; 253 | } 254 | 255 | private JObject SerialiseDistrictMap(DistrictMap districtMap) 256 | { 257 | var jsonObj = SerialiseGenericObject(districtMap, districtMap); 258 | var districtList = districtMap.Districts.Values.ToArray(); 259 | jsonObj.Add(new JProperty("districts", SerialiseList(districtList, districtMap))); 260 | var size = districtMap.Map.Size; 261 | jsonObj.Add(new JProperty("size", new JArray(size.X, size.Y))); 262 | var dataArr = new JArray(); 263 | for (int z = 0; z < size.Y; ++z) 264 | { 265 | var row = new JArray(); 266 | for (int x = 0; x < size.X; ++x) 267 | { 268 | int districtId = districtMap.Map[new Vector2i(x, z)]; 269 | var district = districtMap.GetDistrictByID(districtId); 270 | row.Add(districtList.IndexOf(d => d == district)); 271 | } 272 | dataArr.Add(row); 273 | } 274 | jsonObj.Add(new JProperty("data", dataArr)); 275 | return jsonObj; 276 | } 277 | 278 | private JObject SerialiseDistrict(District district) 279 | { 280 | var jsonObj = SerialiseGenericObject(district); 281 | jsonObj.Add(new JProperty("color", SerialiseValue(district.Color))); 282 | return jsonObj; 283 | } 284 | 285 | private JObject SerialiseBankAccount(BankAccount bankAccount) 286 | { 287 | var jsonObj = SerialiseGenericObject(bankAccount); 288 | var holdingsArr = new JArray(); 289 | foreach (var holding in bankAccount.CurrencyHoldings.Values) 290 | { 291 | if (holding.Val <= 0.0f) { continue; } 292 | var holdingObj = new JObject(); 293 | holdingObj.Add("currency", SerialiseObjectReference(holding.Currency)); 294 | holdingObj.Add("amount", holding.Val); 295 | holdingsArr.Add(holdingObj); 296 | } 297 | jsonObj.Add("holdings", holdingsArr); 298 | return jsonObj; 299 | } 300 | 301 | private JObject SerialiseGameValueContext(IGameValueContext gameValueContext) 302 | { 303 | var jsonObj = new JObject(); 304 | jsonObj.Add(new JProperty("type", "GameValueContext")); 305 | jsonObj.Add(new JProperty("_name", SerialiseValue((gameValueContext as INamed)?.Name ?? ""))); 306 | jsonObj.Add(new JProperty("markedUpName", SerialiseValue((gameValueContext as INamed)?.MarkedUpName ?? ""))); 307 | string contextDescription = gameValueContext.GetType().GetProperty("ContextDescription", BindingFlags.Public | BindingFlags.Instance) 308 | .GetValue(gameValueContext, BindingFlags.NonPublic | BindingFlags.Instance, null, null, null) as string; 309 | jsonObj.Add(new JProperty("contextDescription", SerialiseValue(contextDescription))); 310 | return jsonObj; 311 | } 312 | 313 | #endregion 314 | 315 | #region Deserialisation 316 | 317 | public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) 318 | { 319 | throw new NotImplementedException(); 320 | } 321 | 322 | #endregion 323 | 324 | public override bool CanRead => false; 325 | 326 | public override bool CanConvert(Type objectType) => true; 327 | } 328 | } 329 | -------------------------------------------------------------------------------- /EcoCivicsImportExportMod/CivicBundle.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | using Newtonsoft.Json.Linq; 6 | 7 | namespace Eco.Mods.CivicsImpExp 8 | { 9 | using Core.Systems; 10 | 11 | using Gameplay.Players; 12 | using Gameplay.Civics; 13 | using Gameplay.Settlements; 14 | 15 | using Shared.Utils; 16 | 17 | public readonly struct CivicReference : IEquatable 18 | { 19 | public readonly Type Type; 20 | public readonly string Name; 21 | 22 | public CivicReference(Type type, string name) 23 | { 24 | Type = type; 25 | Name = name; 26 | } 27 | 28 | public IHasID Resolve() 29 | => Registrars.GetByDerivedType(Type).GetByName(Name); 30 | 31 | public override bool Equals(object obj) 32 | => obj is CivicReference reference && Equals(reference); 33 | 34 | public bool Equals(CivicReference other) 35 | => EqualityComparer.Default.Equals(Type, other.Type) 36 | && Name == other.Name; 37 | 38 | public override int GetHashCode() 39 | => HashCode.Combine(Type, Name); 40 | 41 | public static bool operator ==(CivicReference left, CivicReference right) 42 | => left.Equals(right); 43 | 44 | public static bool operator !=(CivicReference left, CivicReference right) 45 | => !(left == right); 46 | 47 | public override string ToString() 48 | => $"{Type.Name}:\"{Name}\""; 49 | } 50 | 51 | public readonly struct BundledCivic 52 | { 53 | public readonly JObject Data; 54 | 55 | public string Name { get => Data.Value("name"); } 56 | 57 | public string TypeName { get => Data.Value("type"); } 58 | 59 | public Type Type 60 | { 61 | get 62 | { 63 | var type = ReflectionUtils.GetTypeFromFullName(TypeName); 64 | if (type == null) { throw new Exception($"Failed to resolve type '{TypeName}'"); } 65 | return type; 66 | } 67 | } 68 | 69 | public CivicReference AsReference { get => new(Type, Name); } 70 | 71 | public IEnumerable References 72 | { 73 | get => SearchForInlineNamedObjects(Data, true, true) 74 | .Select(t => t.Item1) 75 | .Distinct(); 76 | } 77 | 78 | public IEnumerable InlineObjects 79 | { 80 | get => SearchForInlineNamedObjects(Data, false, true) 81 | .Select(t => new BundledCivic(t.Item2)); 82 | } 83 | 84 | public BundledCivic(JObject data) 85 | { 86 | Data = data; 87 | } 88 | 89 | private static IEnumerable<(CivicReference, JObject)> SearchForInlineNamedObjects(JToken target, bool? references = null, bool ignoreRoot = false) 90 | { 91 | if (target is JObject obj) 92 | { 93 | string name = obj.Value("name"); 94 | string typeName = obj.Value("type"); 95 | bool isRef = obj.Value("reference"); 96 | if (!ignoreRoot && !string.IsNullOrEmpty(name) && !string.IsNullOrEmpty(typeName)) 97 | { 98 | if (references == null || references.Value == isRef) 99 | { 100 | yield return (new CivicReference(ReflectionUtils.GetTypeFromFullName(typeName), name), obj); 101 | } 102 | yield break; 103 | } 104 | foreach (var pair in obj) 105 | { 106 | foreach (var referenceTuple in SearchForInlineNamedObjects(pair.Value, references)) 107 | { 108 | yield return referenceTuple; 109 | } 110 | } 111 | } 112 | else if (target is JArray arr) 113 | { 114 | foreach (var element in arr) 115 | { 116 | foreach (var referenceTuple in SearchForInlineNamedObjects(element, references)) 117 | { 118 | yield return referenceTuple; 119 | } 120 | } 121 | } 122 | } 123 | 124 | public IHasID CreateStub() 125 | { 126 | var registrar = Registrars.GetByDerivedType(Type); 127 | if (registrar == null) 128 | { 129 | throw new InvalidOperationException($"No registrar found for type '{Type.FullName}'"); 130 | } 131 | var obj = Activator.CreateInstance(Type) as IHasID; 132 | registrar.Insert(obj); 133 | return obj; 134 | } 135 | 136 | public bool Is() where T : IHasID 137 | { 138 | return typeof(T).IsAssignableFrom(Type); 139 | } 140 | } 141 | 142 | public readonly struct BundledSettlementCivic 143 | { 144 | public readonly BundledCivic BundledCivic; 145 | 146 | public string Name { get => BundledCivic.Name; } 147 | 148 | public string TypeName { get => BundledCivic.TypeName; } 149 | 150 | public Type Type { get => BundledCivic.Type; } 151 | 152 | public CivicReference AsReference { get => BundledCivic.AsReference; } 153 | 154 | public IEnumerable References { get => BundledCivic.References; } 155 | 156 | public IEnumerable InlineObjects { get => BundledCivic.InlineObjects; } 157 | 158 | public CivicReference? LeaderReference { get => GetReferenceProperty(nameof(Settlement.Leader)); } 159 | 160 | public CivicReference? ImmigrationPolicyReference { get => GetReferenceProperty(nameof(Settlement.ImmigrationPolicy)); } 161 | 162 | public CivicReference? ElectionProcessReference { get => GetReferenceProperty(nameof(Settlement.ElectionProcess)); } 163 | 164 | public CivicReference? ConstitutionReference { get => GetReferenceProperty(nameof(Settlement.Constitution)); } 165 | 166 | public CivicReference? CitizenDemographicReference { get => GetReferenceProperty(nameof(Settlement.CitizenDemographic)); } 167 | 168 | public BundledSettlementCivic(BundledCivic bundledCivic) 169 | { 170 | BundledCivic = bundledCivic; 171 | } 172 | 173 | private CivicReference? GetReferenceProperty(string key) 174 | { 175 | var propertiesToken = BundledCivic.Data["properties"]; 176 | if (propertiesToken is not JObject propertiesObj) { return null; } 177 | var referenceToken = propertiesObj[key]; 178 | if (referenceToken is not JObject referenceObj) { return null; } 179 | string name = referenceObj.Value("name"); 180 | string typeName = referenceObj.Value("type"); 181 | bool isRef = referenceObj.Value("reference"); 182 | if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(typeName) || !isRef) { return null; } 183 | var type = ReflectionUtils.GetTypeFromFullName(typeName); 184 | if (type == null) { return null; } 185 | return new CivicReference(type, name); 186 | } 187 | } 188 | 189 | public class CivicBundle 190 | { 191 | #region Importing 192 | 193 | public static CivicBundle LoadFromText(string text) 194 | { 195 | JObject jsonObj = JObject.Parse(text); 196 | string typeName = jsonObj.Value("type"); 197 | var versionArr = jsonObj.Value("version"); 198 | int verMaj = versionArr.Value(0); 199 | //int verMin = versionArr.Value(1); 200 | if (verMaj != CivicsJsonConverter.MajorVersion) 201 | { 202 | throw new InvalidOperationException($"Civic format not supported (found major '{verMaj}', expecting '{CivicsJsonConverter.MajorVersion}')"); 203 | } 204 | if (typeName == typeof(CivicBundle).FullName) 205 | { 206 | // Importing formal bundle with multiple civics 207 | var civics = jsonObj.Value("civics"); 208 | var bundle = new CivicBundle(); 209 | for (int i = 0; i < civics.Count; ++i) 210 | { 211 | var element = civics.Value(i); 212 | bundle.civics.Add(new BundledCivic(element)); 213 | } 214 | return bundle; 215 | } 216 | else 217 | { 218 | // Importing single civic 219 | var bundle = new CivicBundle(); 220 | bundle.civics.Add(new BundledCivic(jsonObj)); 221 | return bundle; 222 | } 223 | } 224 | 225 | #endregion 226 | 227 | private readonly IList civics = new List(); 228 | 229 | public IEnumerable Civics { get => civics; } 230 | 231 | public IEnumerable AllReferences { get => Civics.SelectMany(c => c.References).Distinct(); } 232 | 233 | public IEnumerable AllInlineObjects { get => civics.SelectMany(c => c.InlineObjects); } 234 | 235 | public IEnumerable ExternalReferences { get => AllReferences.Where(r => !ReferenceIsLocal(r)); } 236 | 237 | public bool ContainsSettlement { get => Civics.Any(c => c.Is()); } 238 | 239 | public BundledCivic? Settlement { get => Civics.Select(c => new BundledCivic?(c)).SingleOrDefault(c => c.Value.Is()); } 240 | 241 | public BundledCivic? Constitution { get => Civics.Select(c => new BundledCivic?(c)).SingleOrDefault(c => c.Value.Is()); } 242 | 243 | public IReadOnlyDictionary GetSettlementOverwriteCivics(Settlement targetSettlement) 244 | { 245 | var dict = new Dictionary(); 246 | var importSettlementCivicRaw = Settlement; 247 | if (importSettlementCivicRaw == null) { return dict; } 248 | var importSettlementCivic = new BundledSettlementCivic(importSettlementCivicRaw.Value); 249 | dict.Add(importSettlementCivic.AsReference, targetSettlement); 250 | var leaderRef = importSettlementCivic.LeaderReference; 251 | if (leaderRef != null && targetSettlement.Leader != null && Civics.Any(c => c.AsReference == leaderRef)) { dict.Add(leaderRef.Value, targetSettlement.Leader); } 252 | var immigrationPolicyRef = importSettlementCivic.ImmigrationPolicyReference; 253 | if (immigrationPolicyRef != null && targetSettlement.ImmigrationPolicy != null && Civics.Any(c => c.AsReference == immigrationPolicyRef)) { dict.Add(immigrationPolicyRef.Value, targetSettlement.ImmigrationPolicy); } 254 | var electionProcessRef = importSettlementCivic.ElectionProcessReference; 255 | if (electionProcessRef != null && targetSettlement.ElectionProcess != null && Civics.Any(c => c.AsReference == electionProcessRef)) { dict.Add(electionProcessRef.Value, targetSettlement.ElectionProcess); } 256 | var constitutionRef = importSettlementCivic.ConstitutionReference; 257 | if (constitutionRef != null && targetSettlement.Constitution != null && Civics.Any(c => c.AsReference == constitutionRef)) { dict.Add(constitutionRef.Value, targetSettlement.Constitution); } 258 | var citizenDemographicRef = importSettlementCivic.CitizenDemographicReference; 259 | if (citizenDemographicRef != null && targetSettlement.CitizenDemographic != null && Civics.Any(c => c.AsReference == citizenDemographicRef)) { dict.Add(citizenDemographicRef.Value, targetSettlement.CitizenDemographic); } 260 | return dict; 261 | } 262 | 263 | public bool ReferenceIsLocal(CivicReference reference) 264 | => Civics.Select(c => c.AsReference).Contains(reference) 265 | || AllInlineObjects.Select(c => c.AsReference).Contains(reference); 266 | 267 | public IEnumerable ApplyMigrations() 268 | { 269 | var migrationReport = new List(); 270 | var migrator = new Migrations.MigratorV1(); 271 | foreach (var obj in Civics) 272 | { 273 | if (migrator.ShouldMigrate(obj.Data)) 274 | { 275 | migrationReport.Add($"Applying migrations for {obj.AsReference}..."); 276 | migrator.ApplyMigration(obj.Data, migrationReport); 277 | } 278 | } 279 | return migrationReport; 280 | } 281 | 282 | public IEnumerable ImportAll(Settlement targetSettlement, User importer) 283 | { 284 | var importContext = new ImportContext(); 285 | var importSettlementCivic = Settlement; 286 | if (importSettlementCivic != null) 287 | { 288 | var settlementOverwriteCivics = GetSettlementOverwriteCivics(targetSettlement); 289 | foreach (var pair in settlementOverwriteCivics) 290 | { 291 | importContext.ReferenceMap.Add(pair); 292 | } 293 | importContext.ImportedObjects.AddUniqueRange(settlementOverwriteCivics.Values); 294 | } 295 | try 296 | { 297 | foreach (var civic in Civics) 298 | { 299 | if (importContext.ReferenceMap.ContainsKey(civic.AsReference)) { continue; } 300 | try 301 | { 302 | IHasID stub = importContext.ImportStub(civic); 303 | foreach (var inlineCivic in civic.InlineObjects) 304 | { 305 | importContext.ImportStub(inlineCivic); 306 | } 307 | } 308 | catch (Exception ex) 309 | { 310 | Logger.Error($"Failed to import stub for civic {civic.AsReference}: {ex}"); 311 | throw; 312 | } 313 | } 314 | foreach (var civic in Civics) 315 | { 316 | try 317 | { 318 | importContext.Import(civic, targetSettlement, importer); 319 | } 320 | catch (Exception ex) 321 | { 322 | Logger.Error($"Failed to import civic {civic.AsReference}: {ex}"); 323 | throw; 324 | } 325 | } 326 | } 327 | catch 328 | { 329 | importContext.Clear(); 330 | throw; 331 | } 332 | return importContext.ImportedObjects; 333 | } 334 | } 335 | } 336 | -------------------------------------------------------------------------------- /EcoCivicsImportExportMod/ImportContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using System.Collections.Generic; 4 | 5 | using Newtonsoft.Json.Linq; 6 | 7 | namespace Eco.Mods.CivicsImpExp 8 | { 9 | using Core.Utils; 10 | using Core.Systems; 11 | using Core.Controller; 12 | 13 | using Shared.Items; 14 | using Shared.Localization; 15 | using Shared.Math; 16 | using Shared.Utils; 17 | 18 | using Gameplay.LegislationSystem; 19 | using Gameplay.Civics.GameValues; 20 | using Gameplay.Civics.Misc; 21 | using Gameplay.Civics.Districts; 22 | using Gameplay.GameActions; 23 | using Gameplay.Utils; 24 | using Gameplay.Economy.Money; 25 | using Gameplay.Economy; 26 | using Gameplay.Settlements; 27 | using Gameplay.Aliases; 28 | using Gameplay.Players; 29 | using Eco.Gameplay.Placement; 30 | 31 | public class ImportContext 32 | { 33 | public IList ImportedObjects { get; } = new List(); 34 | 35 | public IDictionary ReferenceMap { get; } = new Dictionary(); 36 | 37 | public IHasID ImportStub(BundledCivic bundledCivic) 38 | { 39 | var obj = bundledCivic.CreateStub(); 40 | ImportedObjects.Add(obj); 41 | ReferenceMap.Add(bundledCivic.AsReference, obj); 42 | return obj; 43 | } 44 | 45 | public void Import(BundledCivic bundledCivic, Settlement settlement, User importer) 46 | { 47 | if (!ReferenceMap.TryGetValue(bundledCivic.AsReference, out IHasID obj)) 48 | { 49 | obj = ImportStub(bundledCivic); 50 | } 51 | if (obj is ISettlementAssociated settlementAssociated) { settlementAssociated.Settlement = settlement; } 52 | if (obj is IHostedObject hostedObject) { hostedObject.Creator = importer; } 53 | if (obj is IProposable proposable) 54 | { 55 | if (proposable.State == ProposableState.Uninitialized) { proposable.InitializeDraftProposable(); } 56 | DeserialiseGenericObject(bundledCivic.Data, obj); 57 | proposable.SetProposedState(proposable.State == ProposableState.Uninitialized ? ProposableState.Draft : proposable.State, true, true); 58 | } 59 | else 60 | { 61 | DeserialiseGenericObject(bundledCivic.Data, obj); 62 | } 63 | } 64 | 65 | public void Clear() 66 | { 67 | Importer.Cleanup(ImportedObjects); 68 | ImportedObjects.Clear(); 69 | ReferenceMap.Clear(); 70 | } 71 | 72 | #region Deserialisation 73 | 74 | private void DeserialiseObjectProperties(object target, JObject obj) 75 | { 76 | foreach (var pair in obj) 77 | { 78 | var targetProperty = target.GetType().GetProperty(pair.Key, BindingFlags.Public | BindingFlags.Instance); 79 | if (targetProperty == null) 80 | { 81 | Logger.Debug($"Json object has property '{pair.Key}' but no such property exists on '{target.GetType().FullName}', skipping"); 82 | continue; 83 | } 84 | DeserialiseValue(target, targetProperty, pair.Value); 85 | } 86 | } 87 | 88 | private void DeserialiseValue(object target, PropertyInfo propertyInfo, JToken value) 89 | { 90 | if (propertyInfo.PropertyType.IsConstructedGenericType && typeof(ControllerList<>).IsAssignableFrom(propertyInfo.PropertyType.GetGenericTypeDefinition())) 91 | { 92 | if (value.Type != JTokenType.Array) 93 | { 94 | Logger.Error($"Can't deserialise {value.Type} into '{target.GetType().FullName}.{propertyInfo.Name}' (expecting Array)"); 95 | return; 96 | } 97 | DeserialiseControllerListOrHashSet(propertyInfo.GetValue(target), value.ToObject()); 98 | } 99 | else if (propertyInfo.SetMethod != null) 100 | { 101 | propertyInfo.SetValue(target, DeserialiseValueAsType(value, propertyInfo.PropertyType)); 102 | } 103 | else 104 | { 105 | if (value == null) 106 | { 107 | Logger.Error($"Can't deserialise value into '{target.GetType().FullName}.{propertyInfo.Name}' as we don't know how (it's a {propertyInfo.PropertyType.FullName} and we got a null)"); 108 | } 109 | else 110 | { 111 | Logger.Error($"Can't deserialise value into '{target.GetType().FullName}.{propertyInfo.Name}' as we don't know how (it's a {propertyInfo.PropertyType.FullName} and we got a {value.Type})"); 112 | } 113 | } 114 | } 115 | 116 | private object DeserialiseValueAsType(JToken token, Type expectedType) 117 | { 118 | if (token.Type == JTokenType.String) 119 | { 120 | var str = token.ToObject().Value as string; 121 | if (expectedType.IsAssignableFrom(typeof(string))) 122 | { 123 | return str; 124 | } 125 | else if (expectedType.IsAssignableFrom(typeof(LocString))) 126 | { 127 | return new LocString(str); 128 | } 129 | else if (expectedType.IsEnum) 130 | { 131 | return Enum.Parse(expectedType, str); 132 | } 133 | } 134 | else if (token.Type == JTokenType.Integer || token.Type == JTokenType.Float || token.Type == JTokenType.Boolean) 135 | { 136 | return Convert.ChangeType(token.ToObject().Value, expectedType); 137 | } 138 | else if (token.Type == JTokenType.Object) 139 | { 140 | JObject obj = token.ToObject(); 141 | string typeName = obj.Value("type"); 142 | return typeName switch 143 | { 144 | "Type" => ResolveType(obj.Value("value")), 145 | "GameValueContext" => DeserialiseGameValueContext(obj, expectedType), 146 | "GameValueWrapper" => DeserialiseGameValueWrapper(obj, expectedType), 147 | "GamePickerList" => DeserialiseGamePickerList(obj, expectedType), 148 | _ => DeserialiseGenericObject(obj, expectedType), 149 | }; 150 | } 151 | else if (token.Type == JTokenType.Array) 152 | { 153 | JArray arr = token.ToObject(); 154 | if (expectedType == typeof(Color)) 155 | { 156 | return new Color(arr.Value(0), arr.Value(1), arr.Value(2), arr.Value(3)); 157 | } 158 | throw new InvalidOperationException($"Can't deserialise an array into a '{expectedType.FullName}'"); 159 | } 160 | else if (token.Type == JTokenType.Null && (expectedType.IsClass || expectedType.IsInterface)) 161 | { 162 | return null; 163 | } 164 | throw new InvalidOperationException($"Can't deserialise a {token.Type} into a '{expectedType.FullName}'"); 165 | } 166 | 167 | private Type ResolveType(string typeName) 168 | => ReflectionUtils.GetTypeFromFullName(typeName); 169 | 170 | public object ResolveReference(CivicReference civicReference) 171 | { 172 | if (ReferenceMap.TryGetValue(civicReference, out IHasID internalObj)) { return internalObj; } 173 | var registrar = Registrars.GetByDerivedType(civicReference.Type); 174 | if (registrar == null) 175 | { 176 | throw new InvalidOperationException($"Can't resolve reference to a '{civicReference.Type.FullName}' ('{civicReference.Name}') as no registrar was found for that type"); 177 | } 178 | var obj = registrar.GetByName(civicReference.Name); 179 | if (obj == null) 180 | { 181 | // Eco bug: treasury bank account is undiscoverable until the server is restarted 182 | if (civicReference.Type == typeof(TreasuryBankAccount) && civicReference.Name == "Treasury Bank Account") 183 | { 184 | return BankAccountManager.Obj.Treasury(); 185 | } 186 | throw new InvalidOperationException($"Failed to resolve reference '{civicReference.Name}' (of type '{civicReference.Type.FullName}')"); 187 | } 188 | return obj; 189 | } 190 | 191 | public bool TryResolveReference(CivicReference civicReference, out object resolvedObject) 192 | { 193 | try 194 | { 195 | resolvedObject = ResolveReference(civicReference); 196 | return true; 197 | } 198 | catch 199 | { 200 | resolvedObject = null; 201 | return false; 202 | } 203 | } 204 | 205 | private object DeserialiseGenericObject(JObject obj, Type expectedType) 206 | { 207 | string typeName = obj.Value("type"); 208 | Type type = ResolveType(typeName); 209 | if (type == null) 210 | { 211 | throw new InvalidOperationException($"Failed to resolve type '{typeName}'"); 212 | } 213 | if (typeof(TriggerConfig).IsAssignableFrom(type)) 214 | { 215 | return DeserialiseTriggerConfig(obj, type, expectedType); 216 | } 217 | string name = obj.Value("name"); 218 | bool isRef = obj.Value("reference"); 219 | if (isRef) 220 | { 221 | if (string.IsNullOrEmpty(name)) 222 | { 223 | throw new InvalidOperationException($"Can't deserialise a reference to '{typeName}' (missing name)"); 224 | } 225 | return ResolveReference(new CivicReference(type, name)); 226 | } 227 | if (!expectedType.IsAssignableFrom(type)) 228 | { 229 | throw new InvalidOperationException($"Can't deserialise a '{typeName}' into a '{expectedType.FullName}'"); 230 | } 231 | object target; 232 | if (!string.IsNullOrEmpty(name) && ReferenceMap.TryGetValue(new CivicReference(type, name), out IHasID existingObj)) 233 | { 234 | target = existingObj; 235 | } 236 | else 237 | { 238 | target = Activator.CreateInstance(type); 239 | if (target is IHasID hasID) 240 | { 241 | var registrar = Registrars.GetByDerivedType(type); 242 | registrar.Insert(hasID); 243 | } 244 | } 245 | DeserialiseGenericObject(obj, target); 246 | return target; 247 | } 248 | 249 | private void DeserialiseGenericObject(JObject obj, object target) 250 | { 251 | string name = obj.Value("name"); 252 | bool isRef = obj.Value("reference"); 253 | if (isRef) 254 | { 255 | throw new InvalidOperationException($"Can't deserialise a reference into an existing object"); 256 | } 257 | if (target is INamed named && !string.IsNullOrEmpty(name)) 258 | { 259 | try 260 | { 261 | var registrar = Registrars.GetByDerivedType(target.GetType()); 262 | registrar.Rename(target as IHasID, name, true); 263 | } 264 | catch (Exception ex) 265 | { 266 | Logger.Debug($"Got unusual error when trying to rename IHasID via registrar: {ex.Message}"); 267 | named.Name = name; 268 | } 269 | } 270 | if (target is SimpleEntry simpleEntry) 271 | { 272 | string description = obj.Value("description"); 273 | if (!string.IsNullOrEmpty(description)) 274 | { 275 | simpleEntry.UserDescription = description; 276 | } 277 | } 278 | if (target is IProposable proposable) 279 | { 280 | proposable.InitializeDraftProposable(); 281 | } 282 | if (target is IHasDualPermissions hasDualPermissions) 283 | { 284 | var managers = obj.Value("managers"); 285 | DeserialiseControllerListOrHashSet(hasDualPermissions.DualPermissions.ManagerSet, managers); 286 | var users = obj.Value("users"); 287 | DeserialiseControllerListOrHashSet(hasDualPermissions.DualPermissions.UserSet, users); 288 | } 289 | if (target is DistrictMap districtMap) 290 | { 291 | var sizeJson = obj.Value("size"); 292 | var size = new Vector2i(sizeJson.Value(0), sizeJson.Value(1)); 293 | if (size != districtMap.Map.Size) 294 | { 295 | throw new InvalidOperationException($"Tried to import district map with a different world size (expecting {districtMap.Map.Size}, got {size})"); 296 | } 297 | var districts = obj.Value("districts"); 298 | var districtList = new List(); 299 | foreach (var districtObj in districts) 300 | { 301 | var district = DeserialiseValueAsType(districtObj, typeof(District)) as District; 302 | districtList.Add(district); 303 | if (district == null) { continue; } 304 | districtMap.Districts.Add(district.Id, district); 305 | } 306 | var rows = obj.Value("data"); 307 | for (int z = 0; z < size.Y; ++z) 308 | { 309 | var row = rows.Value(z); 310 | for (int x = 0; x < size.X; ++x) 311 | { 312 | var localId = row.Value(x); 313 | if (localId >= 0) 314 | { 315 | var district = districtList[localId]; 316 | if (district != null) 317 | { 318 | districtMap.Map[new Vector2i(x, z)] = district.Id; 319 | } 320 | } 321 | } 322 | } 323 | districtMap.Changed(nameof(districtMap.Districts)); 324 | districtMap.Changed(nameof(districtMap.Map)); 325 | districtMap.UpdateDistricts(); 326 | } 327 | if (target is District district2) 328 | { 329 | district2.SetColor((Color)DeserialiseValueAsType(obj.Value("color"), typeof(Color))); 330 | } 331 | if (target is BankAccount bankAccount) 332 | { 333 | var holdings = obj.Value("holdings"); 334 | foreach (var value in holdings) 335 | { 336 | if (value is JObject holding) 337 | { 338 | var currency = DeserialiseGenericObject(holding.Value("currency"), typeof(Currency)) as Currency; 339 | if (currency == null) { continue; } 340 | bankAccount.AddCurrency(currency, holding.Value("amount")); 341 | } 342 | } 343 | } 344 | DeserialiseObjectProperties(target, obj.Value("properties")); 345 | if (target is IProposable proposable2) 346 | { 347 | proposable2.SetProposedState(ProposableState.Draft, true, true); 348 | } 349 | } 350 | 351 | private void DeserialiseControllerListOrHashSet(object target, JArray token) 352 | { 353 | Type innerElementType = target is IClientControlledContainer clientControlledContainer ? clientControlledContainer.Type : target.GetType().GetGenericArguments()[0]; 354 | target.GetType().GetMethod("Clear", BindingFlags.Public | BindingFlags.Instance).Invoke(target, null); 355 | var addMethod = target.GetType().GetMethod("Add", BindingFlags.Public | BindingFlags.Instance, null, new Type[] { innerElementType }, null); 356 | foreach (var element in token) 357 | { 358 | addMethod.Invoke(target, new object[] { DeserialiseValueAsType(element, innerElementType) }); 359 | } 360 | } 361 | 362 | private object DeserialiseGameValueContext(JObject obj, Type expectedType) 363 | { 364 | if (!expectedType.IsConstructedGenericType) 365 | { 366 | throw new InvalidOperationException($"Can't deserialise a GameValueContext into a '{expectedType.FullName}'"); 367 | } 368 | Type innerType = expectedType.GetGenericArguments()[0]; 369 | Type gameValueContextType = typeof(GameValueContext<>).MakeGenericType(innerType); 370 | if (!expectedType.IsAssignableFrom(gameValueContextType)) 371 | { 372 | throw new InvalidOperationException($"Can't deserialise a '{gameValueContextType.FullName}' into a '{expectedType.FullName}'"); 373 | } 374 | var gameValueContext = Activator.CreateInstance(gameValueContextType) as IGameValueContext; 375 | gameValueContextType.GetProperty("Name", BindingFlags.Public | BindingFlags.Instance) 376 | .SetValue(gameValueContext, obj.Value("_name"), BindingFlags.Public | BindingFlags.Instance, null, null, null); 377 | gameValueContextType.GetProperty("MarkedUpNameString", BindingFlags.Public | BindingFlags.Instance) 378 | .SetValue(gameValueContext, obj.Value("markedUpName"), BindingFlags.Public | BindingFlags.Instance, null, null, null); 379 | gameValueContextType.GetProperty("ContextDescription", BindingFlags.Public | BindingFlags.Instance) 380 | .SetValue(gameValueContext, obj.Value("contextDescription"), BindingFlags.Public | BindingFlags.Instance, null, null, null); 381 | (gameValueContext as IController).Changed("Title"); 382 | return gameValueContext; 383 | } 384 | 385 | private object DeserialiseGameValueWrapper(JObject obj, Type expectedType) 386 | { 387 | if (!expectedType.IsConstructedGenericType) 388 | { 389 | throw new InvalidOperationException($"Can't deserialise a GameValueWrapper into a '{expectedType.FullName}'"); 390 | } 391 | Type innerType = expectedType.GetGenericArguments()[0]; 392 | Type gameValueWrapperType = typeof(GameValueWrapper<>).MakeGenericType(innerType); 393 | if (!expectedType.IsAssignableFrom(gameValueWrapperType)) 394 | { 395 | throw new InvalidOperationException($"Can't deserialise a '{gameValueWrapperType.FullName}' into a '{expectedType.FullName}'"); 396 | } 397 | var value = obj.Value("value"); 398 | var gameValueWrapper = Activator.CreateInstance(gameValueWrapperType); 399 | gameValueWrapperType.GetProperty("Object", BindingFlags.Public | BindingFlags.Instance).SetValue(gameValueWrapper, DeserialiseValueAsType(value, innerType)); 400 | return gameValueWrapper; 401 | } 402 | 403 | private GamePickerList DeserialiseGamePickerList(JObject obj, Type expectedType) 404 | { 405 | GamePickerList gamePickerList; 406 | if (expectedType.IsConstructedGenericType) 407 | { 408 | Type innerType = expectedType.GetGenericArguments()[0]; 409 | Type gamePickerListType = typeof(GamePickerList<>).MakeGenericType(innerType); 410 | if (!expectedType.IsAssignableFrom(gamePickerListType)) 411 | { 412 | throw new InvalidOperationException($"Can't deserialise a '{gamePickerListType.FullName}' into a '{expectedType.FullName}'"); 413 | } 414 | gamePickerList = Activator.CreateInstance(gamePickerListType, new object[] { null }) as GamePickerList; 415 | } 416 | else if (expectedType.IsAssignableFrom(typeof(GamePickerList))) 417 | { 418 | gamePickerList = new GamePickerList(); 419 | JObject mustDeriveTypeToken = obj.Value("mustDeriveType"); 420 | if (mustDeriveTypeToken != null) 421 | { 422 | gamePickerList.MustDeriveType = DeserialiseValueAsType(mustDeriveTypeToken, typeof(Type)) as Type; 423 | } 424 | } 425 | else if (expectedType.IsAssignableFrom(typeof(GamePickerListAlias))) 426 | { 427 | gamePickerList = new GamePickerListAlias(); 428 | JObject mustDeriveTypeToken = obj.Value("mustDeriveType"); 429 | if (mustDeriveTypeToken != null) 430 | { 431 | var mustDeriveType = DeserialiseValueAsType(mustDeriveTypeToken, typeof(Type)) as Type; 432 | if (mustDeriveType != typeof(IAlias)) 433 | { 434 | throw new InvalidOperationException($"Can't deserialise a GamePickerList with mustDeriveType of '{mustDeriveType.FullName}' into a '{expectedType.FullName}'"); 435 | } 436 | } 437 | } 438 | else 439 | { 440 | throw new InvalidOperationException($"Can't deserialise a GamePickerList into a '{expectedType.FullName}'"); 441 | } 442 | string requiredTag = obj.Value("requiredTag"); 443 | if (!string.IsNullOrEmpty(requiredTag)) 444 | { 445 | gamePickerList.RequiredTag = requiredTag; 446 | } 447 | var arr = obj.Value("entries"); 448 | foreach (JToken entry in arr) 449 | { 450 | gamePickerList.Entries.Add(DeserialiseValueAsType(entry, typeof(Type))); 451 | } 452 | string internalDescription = obj.Value("internalDescription"); 453 | if (!string.IsNullOrEmpty(internalDescription)) 454 | { 455 | gamePickerList.InternalDescription = internalDescription; 456 | gamePickerList.Changed(nameof(gamePickerList.MarkedUpName)); 457 | } 458 | return gamePickerList; 459 | } 460 | 461 | private TriggerConfig DeserialiseTriggerConfig(JObject obj, Type triggerConfigType, Type expectedType) 462 | { 463 | if (!expectedType.IsAssignableFrom(typeof(TriggerConfig))) 464 | { 465 | throw new InvalidOperationException($"Can't deserialise a TriggerConfig into a '{expectedType.FullName}'"); 466 | } 467 | if (!typeof(TriggerConfig).IsAssignableFrom(triggerConfigType)) 468 | { 469 | throw new InvalidOperationException($"Can't deserialise a '{triggerConfigType.FullName}' into a TriggerConfig"); 470 | } 471 | var triggerConfig = Activator.CreateInstance(triggerConfigType) as TriggerConfig; 472 | typeof(TriggerConfig) 473 | .GetProperty("PropNameBacker", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) 474 | .SetValue(triggerConfig, obj.Value("propNameBacker"), BindingFlags.NonPublic | BindingFlags.Instance, null, null, null); 475 | string typeToConfig = obj.Value("typeToConfig"); 476 | typeof(TriggerConfig) 477 | .GetProperty("TypeToConfig", BindingFlags.NonPublic | BindingFlags.Instance) 478 | .SetValue(triggerConfig, string.IsNullOrEmpty(typeToConfig) ? null : ResolveType(typeToConfig), BindingFlags.NonPublic | BindingFlags.Instance, null, null, null); 479 | string propDisplayName = obj.Value("propDisplayName"); 480 | typeof(TriggerConfig) 481 | .GetProperty("PropDisplayName", BindingFlags.NonPublic | BindingFlags.Instance) 482 | .SetValue(triggerConfig, propDisplayName, BindingFlags.NonPublic | BindingFlags.Instance, null, null, null); 483 | DeserialiseObjectProperties(triggerConfig, obj.Value("properties")); 484 | return triggerConfig; 485 | } 486 | 487 | 488 | #endregion 489 | } 490 | } 491 | -------------------------------------------------------------------------------- /EcoCivicsImportExportMod/CivicsImpExpCommands.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.IO; 4 | using System.Collections.Generic; 5 | using System.Numerics; 6 | using System.Threading.Tasks; 7 | 8 | namespace Eco.Mods.CivicsImpExp 9 | { 10 | using Core.Systems; 11 | 12 | using Shared.Localization; 13 | using Shared.IoC; 14 | using Shared.Items; 15 | using Shared.Voxel; 16 | using Shared.Utils; 17 | 18 | using Gameplay.Players; 19 | using Gameplay.Systems.Messaging.Chat.Commands; 20 | using Gameplay.Systems.TextLinks; 21 | using Gameplay.Civics; 22 | using Gameplay.Civics.Laws; 23 | using Gameplay.Civics.Titles; 24 | using Gameplay.Civics.Demographics; 25 | using Gameplay.Civics.Constitutional; 26 | using Gameplay.Civics.Districts; 27 | using Gameplay.Civics.Misc; 28 | using Gameplay.Civics.Immigration; 29 | using Gameplay.Objects; 30 | using Gameplay.Components; 31 | using Gameplay.Economy; 32 | using Gameplay.Settlements; 33 | using Gameplay.Systems; 34 | using Gameplay.Systems.Chat; 35 | 36 | [ChatCommandHandler] 37 | public static class CivicsImpExpCommands 38 | { 39 | private static readonly IReadOnlyDictionary civicKeyToType = new Dictionary 40 | { 41 | {"law", typeof(Law) }, 42 | {"electionprocess", typeof(ElectionProcess) }, 43 | {"electedtitle", typeof(ElectedTitle) }, 44 | {"appointedtitle", typeof(AppointedTitle) }, 45 | {"demographic", typeof(Demographic) }, 46 | {"constitution", typeof(Constitution) }, 47 | {"amendment", typeof(ConstitutionalAmendment) }, 48 | {"districtmap", typeof(DistrictMap) }, 49 | {"govaccount", typeof(GovernmentBankAccount) }, 50 | {"settlement", typeof(Settlement) }, 51 | {"immigrationpolicy", typeof(ImmigrationPolicy) }, 52 | {"injunction", typeof(Injunction) }, 53 | }; 54 | 55 | private static readonly IReadOnlyDictionary typeToCivicKey = new Dictionary( 56 | civicKeyToType 57 | .Select(pair => new KeyValuePair(pair.Value, pair.Key)) 58 | ); 59 | 60 | private static readonly IReadOnlyDictionary stateNamesToStates = new Dictionary 61 | { 62 | {"draft", ProposableState.Draft }, 63 | {"proposed", ProposableState.Proposed }, 64 | {"active", ProposableState.Active }, 65 | {"removed", ProposableState.Removed }, 66 | }; 67 | 68 | private static bool TryGetTypeForCivicKey(IChatClient chatClient, string civicKey, out Type civicType) 69 | { 70 | if (civicKeyToType.TryGetValue(civicKey, out civicType)) { return true; } 71 | chatClient.Msg(new LocString($"Unknown civic key '{civicKey}' (expecting one of {string.Join(", ", civicKeyToType.Keys.Select(type => $"'{type}'"))})")); 72 | return false; 73 | } 74 | 75 | #region Exporting 76 | 77 | [ChatSubCommand("Civics", "Exports a civic object to a json file.", ChatAuthorizationLevel.Admin)] 78 | public static async Task Export(IChatClient chatClient, int id) 79 | { 80 | if (!UniversalIDs.TryGetByID(id, out IHasUniversalID obj) || obj == null) 81 | { 82 | chatClient.Msg(new LocString($"Failed to export object: none found by id {id}")); 83 | return; 84 | } 85 | if (!typeToCivicKey.TryGetValue(obj.GetType(), out var civicKey)) 86 | { 87 | chatClient.Msg(new LocString($"Failed to export object: type {obj.GetType().Name} not supported by exporter")); 88 | return; 89 | } 90 | string outPath = Path.Combine("civics", $"{civicKey}-{id}.json"); 91 | try 92 | { 93 | await Exporter.Export(obj, outPath); 94 | } 95 | catch (Exception ex) 96 | { 97 | chatClient.Msg(new LocString($"Failed to export {civicKey}: {ex.Message}")); 98 | Logger.Error(ex.ToString()); 99 | return; 100 | } 101 | chatClient.Msg(new LocString($"Exported {civicKey} {id} to '{outPath}'")); 102 | } 103 | 104 | private static async Task<(int successCount, int failCount)> ExportAllOfInternal(IChatClient chatClient, string civicKey, ProposableState? stateFilter) 105 | { 106 | if (!TryGetTypeForCivicKey(chatClient, civicKey, out var civicType)) { return (0, 0); } 107 | int successCount = 0, failCount = 0; 108 | foreach (var obj in Registrars.GetByDerivedType(civicType).All()) 109 | { 110 | if (!civicType.IsAssignableFrom(obj.GetType())) { continue; } 111 | if (stateFilter != null && obj is IProposable proposable && proposable.State != stateFilter.Value) { continue; } 112 | string outPath = Path.Combine(CivicsImpExpPlugin.ImportExportDirectory, $"{civicKey}-{obj.Id}.json"); 113 | try 114 | { 115 | await Exporter.Export(obj, outPath); 116 | ++successCount; 117 | } 118 | catch (Exception ex) 119 | { 120 | chatClient.Msg(new LocString($"Failed to export {civicKey} {obj.Id}: {ex.Message}")); 121 | Logger.Error(ex.ToString()); 122 | ++failCount; 123 | } 124 | } 125 | return (successCount, failCount); 126 | } 127 | 128 | [ChatSubCommand("Civics", "Exports all civic objects of a kind to json files.", ChatAuthorizationLevel.Admin)] 129 | public static async Task ExportAllOf(IChatClient chatClient, string civicKey, string onlyThisState = "") 130 | { 131 | ProposableState? stateFilter = null; 132 | if (!string.IsNullOrEmpty(onlyThisState)) 133 | { 134 | if (!stateNamesToStates.TryGetValue(onlyThisState, out var rawStateFilter)) 135 | { 136 | chatClient.Msg(new LocString($"Invalid civic state '{onlyThisState}'")); 137 | return; 138 | } 139 | stateFilter = rawStateFilter; 140 | } 141 | var (successCount, failCount) = await ExportAllOfInternal(chatClient, civicKey, stateFilter); 142 | if (failCount > 0) 143 | { 144 | if (successCount == 0) 145 | { 146 | chatClient.Msg(new LocString($"Failed to export all {failCount} of {civicKey}")); 147 | } 148 | else 149 | { 150 | chatClient.Msg(new LocString($"successfully exported {successCount} of {civicKey}, but failed to export {failCount} of {civicKey}")); 151 | } 152 | } 153 | else 154 | { 155 | chatClient.Msg(new LocString($"successfully exported all {successCount} of {civicKey}")); 156 | } 157 | } 158 | 159 | [ChatSubCommand("Civics", "Exports all civic objects to json files.", ChatAuthorizationLevel.Admin)] 160 | public static async Task ExportAll(IChatClient clientClient, string onlyThisState = "") 161 | { 162 | ProposableState? stateFilter = null; 163 | if (!string.IsNullOrEmpty(onlyThisState)) 164 | { 165 | if (!stateNamesToStates.TryGetValue(onlyThisState, out var rawStateFilter)) 166 | { 167 | clientClient.Msg(new LocString($"Invalid civic state '{onlyThisState}'")); 168 | return; 169 | } 170 | stateFilter = rawStateFilter; 171 | } 172 | int allSuccessCount = 0, allFailCount = 0; 173 | foreach (var civicKey in civicKeyToType.Keys) 174 | { 175 | var (successCount, failCount) = await ExportAllOfInternal(clientClient, civicKey, stateFilter); 176 | allSuccessCount += successCount; 177 | allFailCount += failCount; 178 | } 179 | if (allFailCount > 0) 180 | { 181 | if (allSuccessCount == 0) 182 | { 183 | clientClient.Msg(new LocString($"Failed to export all {allFailCount} civic objects")); 184 | } 185 | else 186 | { 187 | clientClient.Msg(new LocString($"successfully exported {allSuccessCount} civic objects, but failed to export {allFailCount} of civic objects")); 188 | } 189 | } 190 | else 191 | { 192 | clientClient.Msg(new LocString($"successfully exported all {allSuccessCount} civic objects")); 193 | } 194 | } 195 | 196 | #endregion 197 | 198 | #region Importing 199 | 200 | private static int GetUsedSlotsCount(CivicObjectComponent civicObjectComponent, IDictionary usedSlotsModifierDict = null) 201 | { 202 | if (usedSlotsModifierDict == null || !usedSlotsModifierDict.TryGetValue(civicObjectComponent, out int modifier)) { modifier = 0; } 203 | return civicObjectComponent.UsedSlots + modifier; 204 | } 205 | 206 | private static IEnumerable<(WorldObject worldObject, CivicObjectComponent civicObjectComponent)> GetAllCivicWorldObjects() 207 | => ServiceHolder.Obj.All 208 | .Where((worldObject) => worldObject.HasComponent()) 209 | .SelectMany((worldObject) => worldObject.GetComponents().Select(y => (worldObject, civicObjectComponent: y))); 210 | 211 | private static IEnumerable<(WorldObject worldObject, CivicObjectComponent civicObjectComponent)> GetAllCivicWorldObjects(Type civicType, Settlement settlement) 212 | => GetAllCivicWorldObjects() 213 | .Where((worldObjectAndComp) => worldObjectAndComp.civicObjectComponent.ObjectType.IsAssignableFrom(civicType) && worldObjectAndComp.civicObjectComponent.Settlement == settlement); 214 | 215 | private static (WorldObject worldObject, CivicObjectComponent civicObjectComponent)? FindFreeWorldObjectForCivic(Type civicType, Settlement settlement, IDictionary usedSlotsModifierDict = null, Vector3? nearestTo = null) 216 | { 217 | var relevantWorldObjects = GetAllCivicWorldObjects(civicType, settlement) 218 | .Where((worldObjectAndComp) => GetUsedSlotsCount(worldObjectAndComp.civicObjectComponent, usedSlotsModifierDict) < worldObjectAndComp.civicObjectComponent.MaxCount); 219 | if (!relevantWorldObjects.Any()) { return null; } 220 | if (nearestTo != null) 221 | { 222 | return relevantWorldObjects 223 | .MinBy((worldObjectAndComp) => World.WrappedDistance(worldObjectAndComp.worldObject.Position, nearestTo.Value)); 224 | } 225 | return relevantWorldObjects.First(); 226 | } 227 | 228 | private static int CountFreeSlotsForCivic(Type civicType, Settlement settlement) 229 | { 230 | return GetAllCivicWorldObjects(civicType, settlement) 231 | .Select((worldObjectAndComp) => worldObjectAndComp.civicObjectComponent.MaxCount - worldObjectAndComp.civicObjectComponent.UsedSlots) 232 | .Sum(); 233 | } 234 | 235 | [ChatSubCommand("Civics", "Imports a civic object from a json file.", ChatAuthorizationLevel.Admin)] 236 | public static async Task Import(IChatClient chatClient, string source, Settlement targetSettlement = null) 237 | { 238 | try 239 | { 240 | await ImportInternal(chatClient, source, targetSettlement); 241 | } 242 | catch (Exception ex) 243 | { 244 | Logger.Error(ex.ToString()); 245 | chatClient.Msg(Localizer.Do($"Encountered exception while running import - check server logs for more details.")); 246 | } 247 | } 248 | 249 | private static async Task ImportInternal(IChatClient chatClient, string source, Settlement targetSettlement = null) 250 | { 251 | // Check settlement 252 | if (FeatureConfig.Obj.SettlementEnabled && targetSettlement == null) 253 | { 254 | chatClient.Msg(Localizer.Do($"You must specify a settlement to import into!")); 255 | return; 256 | } 257 | targetSettlement ??= SettlementManager.Obj.LegacySettlement; 258 | 259 | // Import the bundle 260 | CivicBundle bundle; 261 | try 262 | { 263 | bundle = await Importer.ImportBundle(source); 264 | } 265 | catch (Exception ex) 266 | { 267 | chatClient.Msg(Localizer.Do($"Failed to import bundle: {ex.Message}")); 268 | Logger.Error($"Exception while importing from '{source}': {ex}"); 269 | return; 270 | } 271 | 272 | // Determine settlement state 273 | var bundleSettlementCount = bundle.Civics.Count(c => c.Is()); 274 | var bundleConstitutionCount = bundle.Civics.Count(c => c.Is()); 275 | if (bundleSettlementCount > 1) 276 | { 277 | chatClient.Msg(Localizer.DoStr("Bundle contains more than 1 settlement, this is not allowed!")); 278 | return; 279 | } 280 | if (bundleConstitutionCount > 1) 281 | { 282 | chatClient.Msg(Localizer.DoStr("Bundle contains more than 1 constitution, this is not allowed!")); 283 | return; 284 | } 285 | if (!FeatureConfig.Obj.SettlementEnabled) 286 | { 287 | if (bundleSettlementCount > 0) 288 | { 289 | chatClient.Msg(Localizer.DoStr("Bundle is not importable as it contains a settlement and the settlement system is not enabled.")); 290 | return; 291 | } 292 | if (bundleConstitutionCount > 0) 293 | { 294 | chatClient.Msg(Localizer.DoStr("Bundle is not importable as it contains a constitution and the settlement system is not enabled.")); 295 | return; 296 | } 297 | } 298 | 299 | // Fetch settlement overwrite civics 300 | var settlementCivicRefs = new HashSet(); 301 | var settlementCivics = new HashSet(); 302 | var settlementBundledCivic = bundle.Settlement; 303 | if (FeatureConfig.Obj.SettlementEnabled && settlementBundledCivic.HasValue) 304 | { 305 | var settlementOverwriteCivics = bundle.GetSettlementOverwriteCivics(targetSettlement); 306 | foreach (var pair in settlementOverwriteCivics) 307 | { 308 | settlementCivicRefs.Add(pair.Key); 309 | settlementCivics.Add(pair.Value); 310 | } 311 | // TODO: Do we want to popup a notice saying what we're going to do, as this could be a destructive operation? 312 | } 313 | 314 | // Check that there are enough free slots for all the civics in the bundle 315 | var bundledCivicsByType = bundle.Civics 316 | .Where((bundledCivic) => !settlementCivicRefs.Contains(bundledCivic.AsReference)) 317 | .GroupBy((bundledCivic) => bundledCivic.Type) 318 | .Where((grouping) => typeof(IProposable).IsAssignableFrom(grouping.Key)); 319 | foreach (var grouping in bundledCivicsByType) 320 | { 321 | var freeSlots = CountFreeSlotsForCivic(grouping.Key, targetSettlement); 322 | int importCount = grouping.Count(); 323 | if (importCount > freeSlots) 324 | { 325 | chatClient.Msg(Localizer.Do($"Unable to import {importCount} of {grouping.Key.Name} (only {freeSlots} available slots for this civic type)")); 326 | return; 327 | } 328 | } 329 | 330 | // Perform migrations 331 | IEnumerable migrationReport; 332 | try 333 | { 334 | migrationReport = bundle.ApplyMigrations(); 335 | } 336 | catch (Exception ex) 337 | { 338 | chatClient.Msg(Localizer.Do($"Failed to perform migrations on civic: {ex.Message}")); 339 | Logger.Error(ex.ToString()); 340 | return; 341 | } 342 | if (migrationReport.Any()) 343 | { 344 | chatClient.Msg(Localizer.Do($"Some migrations were performed:\n{string.Join("\n", migrationReport)}")); 345 | } 346 | 347 | // Import the objects from the bundle 348 | IEnumerable importedObjects; 349 | try 350 | { 351 | importedObjects = bundle.ImportAll(targetSettlement, chatClient as User); 352 | } 353 | catch (Exception ex) 354 | { 355 | chatClient.Msg(Localizer.Do($"Failed to import civic: {ex.Message}")); 356 | return; 357 | } 358 | CivicsImpExpPlugin.Obj.LastImport.Clear(); 359 | if (!settlementCivicRefs.Any()) 360 | { 361 | CivicsImpExpPlugin.Obj.LastImport.AddRange(importedObjects); 362 | } 363 | 364 | // Notify of import for non-proposables (e.g. bank accounts, appointed titles) 365 | var importReport = new List(); 366 | foreach (var obj in importedObjects.Where((obj) => obj is not IProposable && obj is not IParentedEntry)) 367 | { 368 | if (obj is not ILinkable linkable) { continue; } 369 | importReport.Add(linkable.UILink()); 370 | } 371 | if (importReport.Count > 0) 372 | { 373 | chatClient.Msg(Localizer.Do($"Imported {string.Join(", ", importReport)} from '{source}'")); 374 | } 375 | if (settlementCivicRefs.Any()) 376 | { 377 | chatClient.Msg(Localizer.DoStr($"This operation is not undoable.")); 378 | } 379 | 380 | // Slot each civic into the relevant world object 381 | IDictionary usedSlotsModifierDict = new Dictionary(); 382 | int importProposableCount = 0; 383 | foreach (var obj in importedObjects.Where((obj) => obj is IProposable && obj is not IParentedEntry)) 384 | { 385 | ++importProposableCount; 386 | var proposable = obj as IProposable; 387 | if (obj == targetSettlement) 388 | { 389 | chatClient.Msg(Localizer.Do($"Imported {proposable.UILink()} from '{source}'")); 390 | continue; 391 | } 392 | if (settlementCivics.Contains(obj)) 393 | { 394 | chatClient.Msg(Localizer.Do($"Imported {proposable.UILink()} from '{source}' as core civic of {targetSettlement.UILink()}")); 395 | continue; 396 | } 397 | var user = chatClient as User; 398 | var worldObjectAndCompMaybe = FindFreeWorldObjectForCivic(obj.GetType(), targetSettlement, usedSlotsModifierDict, user?.Position); 399 | if (worldObjectAndCompMaybe == null) 400 | { 401 | // This should never happen as we already checked above for free slots and early'd out, but just in case... 402 | if (!settlementCivicRefs.Any()) { Importer.Cleanup(importedObjects); } 403 | chatClient.Msg(Localizer.Do($"Failed to import civic of type '{obj.GetType().Name}': no world objects found with available space for the civic")); 404 | CivicsImpExpPlugin.Obj.LastImport.Clear(); 405 | return; 406 | } 407 | var (worldObject, civicObjectComponent) = worldObjectAndCompMaybe.Value; 408 | 409 | proposable.AssignHostObject(worldObject); 410 | if (usedSlotsModifierDict.TryGetValue(civicObjectComponent, out int currentModifier)) 411 | { 412 | usedSlotsModifierDict[civicObjectComponent] = currentModifier + 1; 413 | } 414 | else 415 | { 416 | usedSlotsModifierDict.Add(civicObjectComponent, 1); 417 | } 418 | chatClient.Msg(Localizer.Do($"Imported {proposable.UILink()} from '{source}' onto {worldObject.UILink()}")); 419 | } 420 | 421 | // Sanity check 422 | if (importProposableCount == 0 && importReport.Count == 0) 423 | { 424 | chatClient.Msg(Localizer.DoStr($"Nothing imported - was the bundle empty or corrupt?")); 425 | } 426 | } 427 | 428 | [ChatSubCommand("Civics", "Undoes the last imported civic bundle. Use with extreme care.", ChatAuthorizationLevel.Admin)] 429 | public static void UndoImport(IChatClient chatClient) 430 | { 431 | try 432 | { 433 | UndoImportInternal(chatClient); 434 | } 435 | catch (Exception ex) 436 | { 437 | Logger.Error(ex.ToString()); 438 | chatClient.Msg(Localizer.Do($"Encountered exception while running undoimport - check server logs for more details.")); 439 | } 440 | } 441 | 442 | private static void UndoImportInternal(IChatClient chatClient) 443 | { 444 | Importer.Cleanup(CivicsImpExpPlugin.Obj.LastImport); 445 | chatClient.Msg(Localizer.Do($"Deleted {CivicsImpExpPlugin.Obj.LastImport.Count} objects from the last import")); 446 | CivicsImpExpPlugin.Obj.LastImport.Clear(); 447 | } 448 | 449 | [ChatSubCommand("Civics", "Prints details about a civic bundle without actually importing anything.", ChatAuthorizationLevel.Admin)] 450 | public static async Task BundleInfo(IChatClient chatClient, string source, Settlement targetSettlement = null) 451 | { 452 | try 453 | { 454 | await BundleInfoInternal(chatClient, source, targetSettlement); 455 | } 456 | catch (Exception ex) 457 | { 458 | Logger.Error(ex.ToString()); 459 | chatClient.Msg(Localizer.Do($"Encountered exception while running bundleinfo - check server logs for more details.")); 460 | } 461 | } 462 | 463 | private static async Task BundleInfoInternal(IChatClient chatClient, string source, Settlement targetSettlement = null) 464 | { 465 | // Check settlement 466 | if (FeatureConfig.Obj.SettlementEnabled && targetSettlement == null) 467 | { 468 | chatClient.Msg(Localizer.DoStr("You must specify a settlement to import into!")); 469 | return; 470 | } 471 | targetSettlement ??= SettlementManager.Obj.LegacySettlement; 472 | 473 | // Import the bundle 474 | CivicBundle bundle; 475 | try 476 | { 477 | bundle = await Importer.ImportBundle(source); 478 | } 479 | catch (Exception ex) 480 | { 481 | chatClient.Msg(Localizer.Do($"Failed to import bundle: {ex.Message}")); 482 | Logger.Error(ex.ToString()); 483 | return; 484 | } 485 | 486 | // Determine settlement state 487 | var bundleSettlementCount = bundle.Civics.Count(c => c.Is()); 488 | if (bundleSettlementCount > 1) 489 | { 490 | chatClient.Msg(Localizer.DoStr("Bundle contains more than 1 settlement, this is not allowed!")); 491 | return; 492 | } 493 | if (!FeatureConfig.Obj.SettlementEnabled && bundleSettlementCount > 0) 494 | { 495 | chatClient.Msg(Localizer.DoStr("Bundle is not importable as it contains a settlement and the settlement system is not enabled.")); 496 | return; 497 | } 498 | 499 | // Print settlement overwrite civics 500 | var settlementCivics = new HashSet(); 501 | var settlementBundledCivic = bundle.Settlement; 502 | if (FeatureConfig.Obj.SettlementEnabled && settlementBundledCivic.HasValue) 503 | { 504 | var settlementOverwriteCivics = bundle.GetSettlementOverwriteCivics(targetSettlement); 505 | settlementCivics.Add(settlementBundledCivic.Value.AsReference); 506 | chatClient.Msg(Localizer.Do($"Bundle contains a settlement. {targetSettlement.UILink()} will be replaced by '{settlementBundledCivic.Value.Name}'.")); 507 | foreach (var pair in settlementOverwriteCivics) 508 | { 509 | if (pair.Value is ILinkable linkable) 510 | { 511 | chatClient.Msg(Localizer.Do($"- {linkable.UILink()} will be replaced by '{pair.Key.Name}'.")); 512 | } 513 | else if (pair.Value is ILinkable) 514 | { 515 | chatClient.Msg(Localizer.Do($"- {pair.Value.MarkedUpName} will be replaced by '{pair.Key.Name}'.")); 516 | } 517 | settlementCivics.Add(pair.Key); 518 | } 519 | } 520 | 521 | // Print type metrics 522 | var bundledCivicsByType = bundle.Civics 523 | .Where((bundledCivic) => !settlementCivics.Contains(bundledCivic.AsReference)) 524 | .GroupBy((bundledCivic) => bundledCivic.Type) 525 | .Where((grouping) => typeof(IProposable).IsAssignableFrom(grouping.Key)); 526 | foreach (var grouping in bundledCivicsByType) 527 | { 528 | var freeSlots = CountFreeSlotsForCivic(grouping.Key, targetSettlement); 529 | int importCount = grouping.Count(); 530 | chatClient.Msg(Localizer.Do($"Bundle has {importCount} of {grouping.Key.Name} (there are {freeSlots} available slots for this civic type)")); 531 | var subobjectsByType = grouping 532 | .SelectMany((bundledCivic) => bundledCivic.InlineObjects) 533 | .GroupBy((bundledCivic) => bundledCivic.Type); 534 | for (int i = 0, l = subobjectsByType.Count(); i < l; ++i) 535 | { 536 | var subGrouping = subobjectsByType.Skip(i).First(); 537 | chatClient.Msg(Localizer.Do($" - with {importCount} of {subGrouping.Key.Name}")); 538 | } 539 | } 540 | 541 | // Print reference metrics 542 | var importContext = new ImportContext(); 543 | IList resolvableExternalReferences = new List(); 544 | IList unresolvableExternalReferences = new List(); 545 | foreach (var civicReference in bundle.ExternalReferences) 546 | { 547 | if (importContext.TryResolveReference(civicReference, out var resolvedObject)) 548 | { 549 | resolvableExternalReferences.Add(resolvedObject); 550 | } 551 | else 552 | { 553 | unresolvableExternalReferences.Add(civicReference); 554 | } 555 | } 556 | if ((resolvableExternalReferences.Count + unresolvableExternalReferences.Count) == 0) 557 | { 558 | chatClient.Msg(Localizer.Do($"Bundle has no external references.")); 559 | } 560 | else 561 | { 562 | if (resolvableExternalReferences.Count > 0) 563 | { 564 | var resRefStr = string.Join(", ", resolvableExternalReferences.Distinct().Select((obj) => obj is ILinkable linkable ? linkable.UILink().ToString() : obj.ToString())); 565 | chatClient.Msg(Localizer.Do($"Bundle has {resolvableExternalReferences.Count} references to the following: {resRefStr}")); 566 | if (unresolvableExternalReferences.Count > 0) 567 | { 568 | var unresRefStr = string.Join(", ", unresolvableExternalReferences.Distinct().Select((civicRef) => $"{civicRef.Type} \"{civicRef.Name}\"")); 569 | chatClient.Msg(Localizer.Do($"Bundle has {unresolvableExternalReferences.Count} unresolvable external references: {unresRefStr}")); 570 | } 571 | else 572 | { 573 | chatClient.Msg(Localizer.Do($"Bundle has no unresolvable external references.")); 574 | } 575 | } 576 | else 577 | { 578 | var unresRefStr = string.Join(", ", unresolvableExternalReferences.Distinct().Select((civicRef) => $"{civicRef.Type} \"{civicRef.Name}\"")); 579 | chatClient.Msg(Localizer.Do($"Bundle has {unresolvableExternalReferences.Count} unresolvable external references: {unresRefStr}")); 580 | } 581 | } 582 | 583 | } 584 | 585 | #endregion 586 | 587 | [ChatSubCommand("Civics", "Fixes all non-removed civics with missing creators.", ChatAuthorizationLevel.Admin)] 588 | public static void FixMissingCreators(IChatClient chatClient) 589 | { 590 | try 591 | { 592 | FixMissingCreatorsInternal(chatClient); 593 | } 594 | catch (Exception ex) 595 | { 596 | Logger.Error(ex.ToString()); 597 | chatClient.Msg(Localizer.Do($"Encountered exception while running fixmissingcreators - check server logs for more details.")); 598 | } 599 | } 600 | 601 | private static void FixMissingCreatorsInternal(IChatClient chatClient) 602 | { 603 | if (chatClient is not User user) 604 | { 605 | chatClient.Msg(Localizer.Do($"Must be a valid user, RCON is not supported")); 606 | return; 607 | } 608 | IList<(Type, int)> fixCounts = new List<(Type, int)>(); 609 | foreach (var civicType in typeToCivicKey.Keys) 610 | { 611 | var registrar = Registrars.GetByDerivedType(civicType); 612 | int localNumFixed = 0; 613 | foreach (var civicObj in registrar.All()) 614 | { 615 | if (civicObj is not IProposable proposable) { continue; } 616 | if (proposable.State == ProposableState.Uninitialized || proposable.State == ProposableState.Removed) { continue; } 617 | if (proposable.Creator == null) 618 | { 619 | proposable.Creator = user; 620 | ++localNumFixed; 621 | } 622 | } 623 | if (localNumFixed > 0) 624 | { 625 | registrar.Save(); 626 | fixCounts.Add((civicType, localNumFixed)); 627 | } 628 | } 629 | if (fixCounts.Count == 0) 630 | { 631 | chatClient.Msg(Localizer.Do($"No civics with missing creators were found")); 632 | return; 633 | } 634 | chatClient.Msg(Localizer.Do($"Found the following civics and corrected their owner to {user.MarkedUpName}: {string.Join(", ", fixCounts.Select(x => $"{x.Item2} of {x.Item1.Name}"))}")); 635 | } 636 | } 637 | } --------------------------------------------------------------------------------