Plugin Examples

TestPlugin

namespace TestPlugin;

using System.Collections.Generic;

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

using ChuckDeviceController.Data.Abstractions;
using ChuckDeviceController.Data.Common;
using ChuckDeviceController.Extensions.Http;
using ChuckDeviceController.Extensions.Json;
using ChuckDeviceController.Geometry.Models;
using ChuckDeviceController.Plugin;
using ChuckDeviceController.Plugin.EventBus;
using ChuckDeviceController.Plugin.EventBus.Events;
using ChuckDeviceController.Plugin.Services;

using JobControllers;

//http://127.0.0.1:8881/plugin/v1
//http://127.0.0.1:8881/Test

/// <summary>
///     Example plugin demonstrating the capabilities
///     of the plugin system and how it works.
/// </summary>
[
    // Specifies where the 'wwwroot' folder will be if any are used or needed.
    // Possible options: embedded resources, local/external, or none.
    StaticFilesLocation(StaticFilesLocation.Resources, StaticFilesLocation.External),
    // Specify the plugin API key to authorize with the host application.
    PluginApiKey("CDC-328TVvD7o85TNbNhjLE0JysVMbOxjXKT"),
]
public class TestPlugin : IPlugin, IDatabaseEvents, IJobControllerServiceEvents, IUiEvents, ISettingsPropertyEvents
{
    #region Plugin Host Variables

    // Plugin host variables are interface contracts that are used
    // to interact with services the host application has registered
    // and is running. They can be initialized by the constructor
    // using dependency injection or by decorating the field with
    // the 'PluginBootstrapperService' attribute. The host application
    // will look for any fields or properties decorated with the
    // 'PluginBootstrapperService' and initialize them with the
    // related service class.

    // Used for logging messages to the host application from the plugin
    private readonly ILoggingHost _loggingHost;

    // Interacts with the job controller instance service to add new job
    // controllers.
    private readonly IJobControllerServiceHost _jobControllerHost;

    // Retrieve data from the database, READONLY.
    // 
    // When decorated with the 'PluginBootstrapperService' attribute, the
    // property will be initalized by the host's service implementation.
    [PluginBootstrapperService(typeof(IDatabaseHost))]
    private readonly IDatabaseHost _databaseHost = null!;

    // Translate text based on the set locale in the host application.
    private readonly ILocalizationHost _localeHost;

    // Expand your plugin implementation by adding user interface elements
    // and pages to the dashboard.
    // 
    // When decorated with the 'PluginBootstrapperService' attribute, the
    // property will be initalized by the host's service implementation.
    [PluginBootstrapperService(typeof(IUiHost))]
    private readonly IUiHost _uiHost = null!;

    // Manage files local to your plugin's folder using saving and loading
    // implementations.
    // 
    // When decorated with the 'PluginBootstrapperService' attribute, the
    // property will be initalized by the host's service implementation.
    [PluginBootstrapperService(typeof(IFileStorageHost))]
    private readonly IFileStorageHost _fileStorageHost = null!;

    [PluginBootstrapperService(typeof(IConfigurationHost))]
    private readonly IConfigurationHost _configurationHost = null!;

    private readonly IGeofenceServiceHost _geofenceServiceHost;

    private readonly IInstanceServiceHost _instanceServiceHost;

    private readonly IEventAggregatorHost _eventAggregatorHost;

    private readonly IAuthorizeHost _authHost;

    #endregion

    #region Plugin Metadata Properties

    /// <summary>
    /// Gets the name of the plugin to use.
    /// </summary>
    public string Name => "TestPlugin";

    /// <summary>
    /// Gets a brief description about the plugin explaining how it
    /// works and what it does.
    /// </summary>
    public string Description => "Demostrates the capabilities of the plugin system.";

    /// <summary>
    /// Gets the name of the author/creator of the plugin.
    /// </summary>
    public string Author => "versx";

    /// <summary>
    /// Gets the current version of the plugin.
    /// </summary>
    public Version Version => new(1, 0, 0);

    #endregion

    #region Plugin Host Properties

    /// <summary>
    ///     Gets or sets the UiHost host service implementation. This is
    ///     initialized separately from the '_uiHost' field that is decorated.
    /// </summary>
    /// <remarks>
    ///     When decorated with the 'PluginBootstrapperService' attribute, the
    ///     property will be initalized by the host's service implementation.
    /// </remarks>
    [PluginBootstrapperService(typeof(IUiHost))]
    public IUiHost UiHost { get; set; } = null!;

    #endregion

    #region Constructor

    /// <summary>
    ///     Instantiates a new instance of <see cref="IPlugin"/> with the host
    ///     application. It is important to only create one constructor for the
    ///     class that inherits the <see cref="IPlugin"/> interface contract.
    ///     Failure to do so will prevent the plugin from loading.
    /// 
    ///     This is so the host application knows which constructor to use
    ///     when it instantiates an instance with the host handlers for each 
    ///     parameter, essentially dependency injection.
    /// </summary>
    /// <param name="loggingHost">Logging host handler.</param>
    /// <param name="localeHost">Localization host handler.</param>
    /// <param name="jobControllerServiceHost"></param>
    /// <param name="instanceServiceHost"></param>
    /// <param name="geofenceServiceHost"></param>
    /// <param name="eventAggregatorHost"></param>
    /// <param name="authHost"></param>
    public TestPlugin(
        ILoggingHost loggingHost,
        ILocalizationHost localeHost,
        IJobControllerServiceHost jobControllerServiceHost,
        IInstanceServiceHost instanceServiceHost,
        IGeofenceServiceHost geofenceServiceHost,
        IEventAggregatorHost eventAggregatorHost,
        IAuthorizeHost authHost)
    {
        _loggingHost = loggingHost;
        _localeHost = localeHost;
        _jobControllerHost = jobControllerServiceHost;
        _instanceServiceHost = instanceServiceHost;
        _geofenceServiceHost = geofenceServiceHost;
        _eventAggregatorHost = eventAggregatorHost;
        _authHost = authHost;

        //_appHost.Restart();
    }

    #endregion

    #region ASP.NET WebApi Configure Callback Handlers

    /// <summary>
    ///     Configures the application to set up middlewares, routing rules, etc.
    /// </summary>
    /// <param name="appBuilder">
    ///     Provides the mechanisms to configure an application's request pipeline.
    /// </param>
    public void Configure(WebApplication appBuilder)
    {
        _loggingHost.LogInformation($"Configure called");

        //var testService = appBuilder.Services.GetService<IPluginService>();

        // We can configure routing here using 'Minimal APIs' or using Mvc Controller classes
        // Minimal API's Reference: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis?view=aspnetcore-6.0
        appBuilder.Map("/plugin/v1", app =>
        {
            app.Run(async (httpContext) =>
            {
                _loggingHost.LogInformation($"Plugin route called");
                await httpContext.Response.WriteAsync($"Hello from plugin {Name}");
            });
        });

        // Add additional endpoints to list on
        appBuilder.Urls.Add("http://*:1199"); // listen on all interfaces
        appBuilder.Urls.Add("http://+:1199"); // listen on all interfaces
        appBuilder.Urls.Add("http://0.0.0.0:1199"); // listen on all interfaces
        appBuilder.Urls.Add("http://localhost:1199");
        appBuilder.Urls.Add("http://127.0.0.1:1199");
        appBuilder.Urls.Add("http://10.0.0.2:1199");

        // Example routing using minimal APIs
        appBuilder.Map("example/{name}", async (httpContext) =>
        {
            Console.WriteLine($"Method: {httpContext.Request.Method}");
            var routeValues = httpContext.Request.RouteValues;
            var name = Convert.ToString(routeValues["name"]);
            Console.WriteLine($"Name: {name}");
            await httpContext.Response.WriteAsync(name!);
        });
        appBuilder.MapGet("example", () => "Hi :)");
        appBuilder.MapPost("example", async (httpContext) =>
        {
            var body = await httpContext.Request.ReadBodyAsStringAsync();
            _loggingHost.LogDebug($"Body: {body}");
            var coords = body?.FromJson<List<Coordinate>>();
            var response = string.Join(", ", coords ?? new());
            _loggingHost.LogDebug($"Coords: {response}");
            await httpContext.Response.WriteAsync(response);
        });
        //appBuilder.MapPut("example", async (httpContext) => { });
        //appBuilder.MapDelete("example", async (httpContext) => { });

        appBuilder.MapGet("example/hello/{name}", async (httpContext) =>
        {
            var method = httpContext.Request.Method;
            var path = httpContext.Request.Path;
            var queryValues = httpContext.Request.Query;
            // httpContext.Request.Form will throw an exception if 'Content-Type' is not 'application/application/www-x-form-urlencoded'
            //var formValues = httpContext.Request.Form;
            var routeValues = httpContext.Request.RouteValues;
            var body = httpContext.Request.Body;
            var userClaims = httpContext.User;
            await httpContext.Response.WriteAsync($"Hello, {routeValues["name"]}!");
        });
        appBuilder.MapGet("example/buenosdias/{name}", async (httpContext) =>
            await httpContext.Response.WriteAsync($"Buenos dias, {httpContext.Request.RouteValues["name"]}!"));
        appBuilder.MapGet("example/throw/{message?}", (httpContext) =>
            throw new Exception(Convert.ToString(httpContext.Request.RouteValues["message"]) ?? "Uh oh!"));
        appBuilder.MapGet("example/{greeting}/{name}", async (httpContext) =>
            await httpContext.Response.WriteAsync($"{httpContext.Request.RouteValues["greeting"]}, {httpContext.Request.RouteValues["name"]}!"));

        // NOTE: Uncommenting the below routing map will overwrite the default '/' routing path to the dashboard
        //appBuilder.MapGet("", async (httpContext) => await httpContext.Response.WriteAsync("Hello, World!"));


        // Register custom middlewares
        // Built in ASP.NET Core Middlewares: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis?view=aspnetcore-6.0#aspnet-core-middleware
        //appBuilder.Use(async (httpContext, next) =>//(HttpContext httpContext, RequestDelegate req, Task next) =>
        //{
        //    // Action before next delegate
        //    await next.Invoke();
        //    // Action after called middleware
        //});

        // Use built in logger from dependency injection
        appBuilder.Logger.LogInformation($"Logging from the plugin '{Name}'");
    }

    /// <summary>
    ///     Register services into the IServiceCollection to use with Dependency Injection.
    ///     This method is called first before the 'Configure(IApplicationBuilder)' method.
    /// 
    ///     Register service(s) with Mvc using dependency injection. Services can be passed to
    ///     other services via the constructor. Depending on the service, you can register the
    ///     service lifetime as 'Singleton', 'Transient', or 'Scoped'.
    /// 
    ///
    ///     - Transient objects are always different.The transient OperationId value is different in the IndexModel and in the middleware.
    ///     - Scoped objects are the same for a given request but differ across each new request.
    ///     - Singleton objects are the same for every request.
    ///     
    ///     More details: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-6.0#constructor-injection-behavior
    /// </summary>
    /// <param name="services">
    ///     Specifies the contract for a collection of service descriptors.
    /// </param>
    public void ConfigureServices(IServiceCollection services)
    {
        _loggingHost.LogInformation($"ConfigureServices called");

        //services.AddDbContext<TodoDbContext>(options => options.UseInMemoryDatabase("todo"), ServiceLifetime.Scoped);
    }

    /// <summary>
    ///     Provides an opportunity for plugins to configure Mvc Builder.
    /// </summary>
    /// <param name="mvcBuilder">
    ///     IMvcBuilder instance that can be configured.
    /// </param>
    public void ConfigureMvcBuilder(IMvcBuilder mvcBuilder)
    {
        _loggingHost.LogInformation($"ConfigureMvcBuilder called");

        // Configure localization for Views
        mvcBuilder
            .AddViewLocalization(
                LanguageViewLocationExpanderFormat.Suffix, options => 
                    options.ResourcesPath = "Resources")
            .AddDataAnnotationsLocalization();
    }

    #endregion

    #region Plugin Event Handlers

    /// <summary>
    ///     Called when the plugin is loaded and registered with the host application.
    ///     Loading UI elements here is the preferred location.
    /// </summary>
    public async void OnLoad()
    {
        _loggingHost.LogInformation($"{Name} v{Version} by {Author} initialized!");

        // Execute IFileStorageHost method tests
        TestFileStorageHost();

        // Execute IConfigurationHost method tests
        //TestConfigurationHost();

        // Add dashboard stats
        var stats = new List<IDashboardStatsItem>
        {
            new DashboardStatsItem("Test", "100", isHtml: false),
            new DashboardStatsItem("Test2", "<b><u>1,000</u></b>", isHtml: true),
            //new DashboardStatsItem("Test3", "<b>2,000</b>", isHtml: false),
        };
        await _uiHost.AddDashboardStatisticsAsync(stats);

        // Register new sidebar headers
        var pluginSidebarItems = new List<SidebarItem>
        {
            new(
                // Dropdown header text that is displayed in the sidebar
                text: "Test",
                // Dropdown header display index in the sidebar
                displayIndex: 0,
                // Dropdown header Fontawesome icon
                icon: "fa-solid fa-fw fa-microscope",
                // Yes we want this to be used as a dropdown and not just
                // a single sidebar entry
                isDropdown: true,
                // List of children sidebar item
                dropdownItems: new List<SidebarItem>
                {
                    // Sidebar item #1
                    new("Page", "Test", "Index", displayIndex: 0, icon: "fa-solid fa-fw fa-vial"),
                    // Sidebar item #2
                    new(
                        // Text that is displayed in the sidebar
                        "Details",
                        // 'Test' is the MVC view controller 'TestController.cs'
                        "Test",
                        // 'Details' is the controller action (method name) that is executed when the navbar header is clicked
                        "Details",
                        // Display index in the sidebar
                        displayIndex: 1,
                        // Fontawesome icon to include (optional)
                        icon: "fa-solid fa-fw fa-hammer",
                        // Whether the sidebar item is disabled and not clickable
                        isDisabled: true
                    ),
                }
            ),
            new SidebarItem
            {
                Text = "Sep",
                DisplayIndex = 998,
                IsSeparator = true,
            },
        };
        await _uiHost.AddSidebarItemsAsync(pluginSidebarItems);

        // Add/register dashboard tiles
        var pluginTile = new DashboardTile
        (
            text: "Test",
            value: "5,000",
            icon: "fa-solid fa-fw fa-hammer",
            controllerName: "Test",
            actionName: "Index"
        );
        await _uiHost.AddDashboardTileAsync(pluginTile);

        var settingsTab = new SettingsTab
        {
            Id = "test",
            Text = "TestPlugin",
            Anchor = "test",
            DisplayIndex = 0,
        };
        await _uiHost.AddSettingsTabAsync(settingsTab);

        var settingsProperties = new List<SettingsProperty>
        {
            new("Enabled", "test-enabled", SettingsPropertyType.CheckBox, true),
            new("First Name", "FirstName", SettingsPropertyType.Text, "Jeremy", displayIndex: 1),
            new("TextAreaTest", "TextAreaTest", SettingsPropertyType.TextArea, "Testing", displayIndex: 2),
            new()
            {
                Text = "Year",
                Name = "Year",
                Value = 2022,
                Type = SettingsPropertyType.Number,
                DisplayIndex = 3,
            },
            new()
            {
                Text = "Geofences",
                Name = "Geofences",
                Value = new List<string> { "Paris", "London", "Sydney" },
                Type = SettingsPropertyType.Select,
                DisplayIndex = 0,
            },
        };
        await _uiHost.AddSettingsPropertiesAsync(settingsTab.Id, settingsProperties);

        TestLocaleHost();

        TestJobControllerServiceHost();

        TestDatabaseHost();

        //_eventAggregatorHost.Subscribe(new PluginObserver());
        _eventAggregatorHost.Publish(new PluginEvent("test message from plugin"));

        await TestAuthorizeHost();
    }

    /// <summary>
    ///     Called when the plugin has been reloaded by the host application.
    /// </summary>
    public void OnReload()
    {
        _loggingHost.LogInformation($"[{Name}] OnReload called");
        // TODO: Reload/re-register UI elements that might have been removed
    }

    /// <summary>
    ///     Called when the plugin has been stopped by the host application.
    /// </summary>
    public void OnStop() => _loggingHost.LogInformation($"[{Name}] OnStop called");

    /// <summary>
    ///     Called when the plugin has been removed by the host application.
    /// </summary>
    public void OnRemove() => _loggingHost.LogInformation($"[{Name}] Onremove called");

    /// <summary>
    ///     Called when the plugin's state has been
    ///     changed by the host application.
    /// </summary>
    /// <param name="state">Plugin's current state</param>
    public void OnStateChanged(PluginState state) =>
        _loggingHost.LogInformation($"[{Name}] Plugin state has changed to '{state}'");

    #endregion

    #region IDatabase Event Handlers

    public void OnStateChanged(DatabaseConnectionState state)
    {
        _loggingHost.LogInformation($"[{Name}] Plugin database connection state has changed: {state}");
    }

    public void OnEntityAdded<T>(T entity)
    {
        _loggingHost.LogInformation($"[{Name}] Plugin database entity has been added: {entity}");
    }

    public void OnEntityModified<T>(T oldEntity, T newEntity)
    {
        _loggingHost.LogInformation($"[{Name}] Plugin database entity has been modified: {oldEntity}->{newEntity}");
    }

    public void OnEntityDeleted<T>(T entity)
    {
        _loggingHost.LogInformation($"[{Name}] Plugin database entity has been deleted: {entity}");
    }

    #endregion

    #region ISettingsProperty Event Handlers

    public void OnSave(IReadOnlyDictionary<string, List<ISettingsProperty>> properties)
    {
        _loggingHost.LogInformation($"[{Name}] Plugin settings saved: {properties.Count:N0}");
    }

    #endregion

    #region Private Methods

    private void TestFileStorageHost()
    {
        var fileName = Name + ".deps.json";

        // Load dependencies config for plugin
        var fileData = _fileStorageHost.Load<DependenciesConfig>("", fileName);
        _loggingHost.LogInformation($"Loaded file data from '{fileName}': {fileData}");

        // Save dependencies config to new folder 'configs' in this plugins folder
        var fileSaveResult = _fileStorageHost.Save(fileData, "configs", fileName);
        _loggingHost.LogInformation($"Saved file data for '{fileName}': {fileSaveResult}");
    }

    private void TestConfigurationHost()
    {
        //var config = _configurationProviderHost.GetConfiguration<Dictionary<string, string>>(sectionName: "ConnectionStrings");
        var config = _configurationHost.GetConfiguration();
        var value = _configurationHost.GetValue<bool>("Enabled", sectionName: "Authentication:GitHub");
        _loggingHost.LogInformation($"Configuration: {config}, Value: {value}");

        var locale = _configurationHost.GetValue<string>("Locale");
        _loggingHost.LogInformation($"Configuration Locale: {locale}");
    }

    private async void TestDatabaseHost()
    {
        try
        {
            // Retrieve database entities 
            var device = await _databaseHost.FindAsync<IDevice, string>("SGV7SE");
            _loggingHost.LogInformation($"Device: {device?.Uuid}");

            var instance = await _databaseHost.FindAsync<IInstance, string>("TestInstance");
            var circleRouteType = instance?.Data?.CircleRouteType;
            _loggingHost.LogInformation($"Instance: {instance?.Name}");

            var geofence = await _databaseHost.FindAsync<IGeofence, string>("Upland");
            _loggingHost.LogInformation($"Geofence: {geofence?.Name}");
            _loggingHost.LogInformation($"Area: {geofence?.Data?.Area}");

            //var devices = await _databaseHost.GetListAsync<IDevice>();
            //_loggingHost.LogMessage($"Devices: {devices.Count}");

            //var device = await _databaseHost.Devices.GetByIdAsync("SGV7SE");
            //_loggingHost.LogMessage($"Device: {device}");

            //var accounts = await _databaseHost.Accounts.GetListAsync();
            //var accounts = await _databaseHost.GetListAsync<IAccount>();
            //_loggingHost.LogMessage($"Accounts: {accounts.Count}");
            //var pokestop = await _databaseHost.GetByIdAsync<IPokestop, string>("0192086043834f1c9c577a54a7890b32.16");
            //_loggingHost.LogMessage($"Pokestop: {pokestop.Name}");

            //var spawnpoints = await _databaseHost.GetAllAsync<ISpawnpoint>();
            //_loggingHost.LogDebug($"Spawnpoints: {spawnpoints.Count:N0}");

            //spawnpoints = await _databaseHost.FindAsync<ISpawnpoint, ulong>(
            //    spawnpoint => spawnpoint.DespawnSecond == null,
            //    spawnpoint => spawnpoint.Id,
            //    SortOrderDirection.Asc,
            //    50
            //);
            //_loggingHost.LogDebug($"Spawnpoints Exp: {spawnpoints?.Count:N0}");

            //var bannedAccounts = await _databaseHost.FindAsync<IAccount, string>(
            //    account => account.Failed == "suspended",
            //    account => account.Username,
            //    SortOrderDirection.Desc,
            //    10000
            //);
            //_loggingHost.LogInformation($"Banned Accounts: {bannedAccounts?.Count:N0}");
        }
        catch (Exception ex)
        {
            _loggingHost.LogError(ex);
        }
    }

    private void TestLocaleHost()
    {
        // Translate 1 to Bulbasaur
        var translated = _localeHost.GetPokemonName(1);
        _loggingHost.LogInformation($"Pokemon: {translated}");
    }

    private async void TestJobControllerServiceHost()
    {
        try
        {
            var customInstanceType = "test_controller";

            // Register custom job controller type TestInstanceController
            await _jobControllerHost.RegisterJobControllerAsync<TestInstanceController>(customInstanceType);

            // Create geofence entity
            //var geofence = CreateGeofence();
            //await _geofenceServiceHost.CreateGeofenceAsync(geofence);

            //var instance = CreateInstance(customInstanceType, new() { geofence.Name });
            //await _instanceServiceHost.CreateInstanceAsync(instance);

            //TestAssignDevice(instance.Name);
        }
        catch (Exception ex)
        {
            _loggingHost.LogError(ex);
        }
    }

    private async void TestAssignDevice(string instanceName)
    {
        // Assign device to new instance using custom job controller
        var uuid = "RH2SE"; //"SGV7SE";
        var device = await _databaseHost.FindAsync<IDevice, string>(uuid);
        if (device == null)
        {
            _loggingHost.LogError($"Failed to get device from database with UUID '{uuid}'");
            return;
        }

        if (device.InstanceName != instanceName)
        {
            await _jobControllerHost.AssignDeviceToJobControllerAsync(device, instanceName);
        }
    }

    private async Task TestAuthorizeHost()
    {
        var roleName = "TestRole";
        var result = await _authHost.RegisterRole(roleName, 3);
        _loggingHost.LogInformation($"Role Result: {result}");
    }

    private static Geofence CreateGeofence()
    {
        var geofence = new Geofence
        {
            Name = "TestGeofence",
            Type = GeofenceType.Circle,
            Data = new GeofenceData
            {
                Area = new List<Coordinate>
                {
                    new Coordinate(34.01, -117.01),
                    new Coordinate(34.02, -117.02),
                    new Coordinate(34.03, -117.03),
                    new Coordinate(34.04, -117.04),
                },
                ["test"] = "123", // <- Add custom properties
            },
        };
        return geofence;
    }

    private static Instance CreateInstance(string customInstanceType, List<string> geofences)
    {
        // Create instance
        var instance = new Instance
        {
            Name = "TestInstance",
            MinimumLevel = 30,
            MaximumLevel = 39,
            Geofences = geofences,
            Data = new InstanceData
            {
                CustomInstanceType = customInstanceType,
                ["test"] = "123", // <- Add custom properties
            },
        };
        return instance;
    }

    #endregion
}

// Mock {file}.deps.json configuration file model classes.
// 
// Since we are passing generic <T> type from host application to
// plugin (and vice versx) we do not need to register classes
// with host using 'PluginServiceAttribute' attribute decoration.
public class DependenciesConfig
{
    public RuntimeTarget RuntimeTarget { get; set; } = new();

    public Dictionary<string, object> CompilationOptions { get; set; } = new();

    public Dictionary<string, Dictionary<string, Dictionary<string, TargetDependencies>>> Targets { get; set; } = new();

    public Dictionary<string, Dictionary<string, object>> Libraries { get; set; } = new();
}

public class RuntimeTarget
{
    public string? Name { get; set; }

    public string? Signature { get; set; }
}

public class TargetDependencies
{
    public Dictionary<string, object> Dependencies { get; set; } = new();
}

public class Instance : IInstance
{
    public string Name { get; set; } = null!;

    public InstanceType Type => InstanceType.Custom;

    public ushort MinimumLevel { get; set; }

    public ushort MaximumLevel { get; set; }

    public List<string> Geofences { get; set; } = new();

    public InstanceData? Data { get; set; } = new();
}

public class Geofence : IGeofence
{
    public string Name { get; set; } = null!;

    public GeofenceType Type { get; set; }

    public GeofenceData? Data { get; set; } = new();
}