Salta ai contenuti

Scrivere un plugin

Un plugin è un assembly con una classe pubblica che implementa IMoongatePlugin, collocato sotto <root>/plugins/<bundle>/. Registra servizi nel container dell’host prima dell’avvio del server; l’host li risolve e avvia allo stesso modo dei propri servizi incorporati.

Questa pagina ti porta da una libreria di classi vuota a un bundle distribuito. L’esempio completo, samples/Moongate.Sample.Plugin/, registra un’entità di persistenza, un modulo e un enum Lua, un comando console e un provider di metriche, e la suite di test lo carica tramite il vero loader dei plugin.

Ogni plugin implementa IMoongatePlugin da Moongate.Server.Core.Interfaces.Plugins:

public interface IMoongatePlugin
{
MoongatePluginData Metadata { get; }
void Register(Container container);
}

Metadata è un record MoongatePluginData costruito come new(id, name, version, author?, description?, dependencies?). id è il valore a cui si riferiscono ogni dipendenza e messaggio di errore. Viene confrontato senza distinguere maiuscole e minuscole e CODE_CONVENTION.md §10 ne fissa la forma: dominio inverso, com.github.author.Moongate.plugins.name. L’esempio usa com.github.moongate-community.moongate.plugins.greeter.

dependencies è una lista di MoongatePluginDependencyData(id, minimumVersion?). minimumVersion è inclusiva. Una lista con due voci per lo stesso ID è rifiutata dal costruttore MoongatePluginData stesso, prima che il plugin raggiunga il registro.

Register dichiara servizi e impostazioni gestite dall’host. Il codice del plugin non deve avviare lavoro, aprire file o socket, o accedere al database; l’host avvia i servizi più tardi, in ordine di priorità. AddConfig<T>() è l’eccezione esplicita per gli helper dell’host: associa la configurazione già analizzata e può aggiungere al file i valori predefiniti di una sezione mancante durante la registrazione. Usa questo helper per la configurazione invece di eseguire I/O su file direttamente (vedi Cosa può fare Register).

Terminal window
dotnet new classlib -n MyShard.Plugin

Poi modifica il .csproj generato:

<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<EnableDynamicLoading>true</EnableDynamicLoading>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Moongate.Server.Core" Version="0.17.0" ExcludeAssets="runtime" />
<PackageReference Include="Moongate.Scripting" Version="0.17.0" ExcludeAssets="runtime" />
</ItemGroup>
</Project>

Fissa la versione distribuita dal tuo host; quella corrente è su nuget.org/packages/Moongate.Server.Core.

ExcludeAssets="runtime" esclude la .dll propria di ogni pacchetto dall’output di compilazione. L’host carica già Moongate.Server.Core.dll, Moongate.Hosting.dll e Moongate.Scripting.dll, quindi una copia nel bundle sarebbe peso inutile. Un pacchetto non distribuito dall’host non deve avere quell’attributo, così il suo assembly finisce nel bundle. I plugin che registrano entità persistite referenziano anche Moongate.Persistence allo stesso modo; l’host fornisce e controlla l’identità dei propri contratti condivisi FreeSql e Npgsql.

EnableDynamicLoading fa copiare all’SDK le dipendenze NuGet private accanto all’output di compilazione; senza, una dipendenza non distribuita dall’host manca nel bundle. Nomina il progetto come il bundle da distribuire (MyShard.Plugin/ che produce plugins/MyShard.Plugin/): il loader si aspetta che l’assembly di ingresso abbia il nome della directory del bundle.

Il plugin utile più piccolo registra un comando console:

Crea MyShard.Plugin/MyShardPlugin.cs:

using DryIoc;
using Moongate.Server.Core.Data.Commands;
using Moongate.Server.Core.Data.Plugins;
using Moongate.Server.Core.Extensions;
using Moongate.Server.Core.Interfaces.Commands;
using Moongate.Server.Core.Interfaces.Plugins;
using Moongate.Server.Core.Types.Accounts;
using Moongate.Server.Core.Types.Commands;
namespace MyShard.Plugin;
public sealed class MyShardPlugin : IMoongatePlugin
{
public MoongatePluginData Metadata { get; } = new(
"com.github.myshard.moongate.plugins.hello",
"Hello",
new Version(1, 0)
);
public void Register(Container container)
{
container.RegisterCommand<HelloCommand>(
"hello",
"Prints a greeting: hello <name>.",
CommandSourceType.Console,
AccountType.Regular
);
}
}

Crea MyShard.Plugin/HelloCommand.cs:

using Moongate.Server.Core.Data.Commands;
using Moongate.Server.Core.Interfaces.Commands;
namespace MyShard.Plugin;
public sealed class HelloCommand : ICommandExecutor
{
public Task ExecuteAsync(CommandContext context)
{
if (context.Arguments.Length != 1)
{
context.PrintError("Usage: hello <name>");
return Task.CompletedTask;
}
context.Print($"Hello, {context.Arguments[0]}!");
return Task.CompletedTask;
}
}

La classe del plugin richiede un costruttore pubblico senza parametri; il loader la istanzia prima che esista qualsiasi container. In un plugin reale, metti ogni tipo nel proprio file.

Rimuovi il file generato Class1.cs; i due file sopra lo sostituiscono. Usa un SDK .NET 10 e una shell Unix. Imposta MOONGATE_ROOT sotto sulla root inizializzata del server. Arresta un server già in esecuzione con shutdown e attendi che termini prima di copiare il bundle. Dalla directory che contiene MyShard.Plugin/, esegui:

Terminal window
dotnet build MyShard.Plugin/MyShard.Plugin.csproj -c Release
export MOONGATE_ROOT="$HOME/moongate"
mkdir -p "$MOONGATE_ROOT/plugins/MyShard.Plugin"
cp -R MyShard.Plugin/bin/Release/net10.0/. "$MOONGATE_ROOT/plugins/MyShard.Plugin/"

La directory del bundle deve contenere MyShard.Plugin.dll, MyShard.Plugin.deps.json e tutte le dipendenze private dell’output di compilazione. Riavvia il server installato:

Terminal window
moongate --root-directory "$MOONGATE_ROOT"

Con un checkout dei sorgenti, esegui invece questo comando dalla root del repository:

Terminal window
dotnet run --project src/Moongate.Server -- --root-directory "$MOONGATE_ROOT"

Nella console locale del server, premi * per sbloccare l’input, poi digita:

hello Moongate

L’output è Hello, Moongate!. hello senza argomento riporta Usage: hello <name>. Questo comando è disponibile solo nella console; non richiede un login. Se il server rifiuta il bundle, consulta Errori che impediscono l’avvio. Le regole di caricamento degli assembly e la compatibilità delle dipendenze sono in Deployment e caricamento.

Il Register dell’esempio in SamplePlugin.cs mostra gli altri helper affiancati: AddPersistenceWorld<GreetingNote>() registra un’entità Realm e la sua facade dati, RegisterInstance(new GreetingCounter()) condivide un’istanza tra un modulo Lua e un provider di metriche, AddScriptModule<GreeterModule>() e RegisterScriptEnum<Tone>() pubblicano una tabella e un enum Lua, e AddMetricProvider<GreetingMetricProvider>() aggiunge campioni allo snapshot diagnostico. La registrazione non si connette mai al database né lo modifica; l’host valida tutte le registrazioni dei plugin come insieme e controlla la disponibilità dello schema prima di risolvere qualsiasi servizio di avvio.

I plugin vengono eseguiti nel ruolo configurato del server. Registra entità Auth e servizi account solo in login/standalone, ed entità World, moduli Lua del mondo e servizi game solo in game/standalone. La registrazione di persistenza per una destinazione inattiva fallisce all’avvio; non si connette al database dell’altro ruolo.

Distribuire SQL versionato con un plugin di persistenza

Section titled “Distribuire SQL versionato con un plugin di persistenza”

Un plugin che registra entità con AddPersistenceAuth<T>() o AddPersistenceWorld<T>() distribuisce l’SQL revisionato accanto alla propria DLL, sotto migrations/ con un manifest.json contenente un ID stabile del componente, e copia quella directory nell’output tramite un elemento <Content Include="migrations/**/*">. Il runner delle migrazioni scopre l’SQL senza caricare assembly; il core viene eseguito prima, poi i componenti dei plugin in ordine ordinale. Vedi File SQL versionati per struttura e regole, e Creare un’entità persistente per un percorso eseguibile dalla classe entità alla migrazione applicata.

Helper di registrazione Cosa registra Documentato in
AddMoongateService<TService, TImpl>(priority) / AddMoongateService<TService>(instance) Un servizio singleton; se l’implementazione implementa anche IMoongateStartupService, si avvia automaticamente alla priority indicata e si arresta in ordine inverso. Altri overload accettano factory e tipi runtime questa pagina
RegisterCommand<TExecutor>(name, description, source, minimumAccountType, descriptionMessage) Un esecutore di comandi console/in gioco, come singleton Comandi console
RegisterPacketHandler<TPacket, THandler>() Un gestore singleton associato a un tipo di pacchetto in ingresso questa pagina
RegisterIncomingPacket<TPacket>() Un tipo di pacchetto in ingresso aggiunto al registro che l’host costruisce all’avvio, così il server può riconoscere i confini e decodificare l’opcode Pacchetti e gestori
RegisterAsyncPacketHandler<TPacket, THandler>() Un gestore asincrono singleton per I/O; i risultati tornano al game loop tramite PacketContext Pacchetti e gestori
OnEvent<TEvent>(handler) Un’iscrizione Func<TEvent, CancellationToken, Task> a un esatto tipo IMoongateEvent, mantenuta per la durata del container questa pagina
AddScriptModule<T>() / RegisterScriptEnum<T>() Una classe [ScriptModule] come singleton, pubblicata in Lua; oppure un enum pubblicato come tabella globale in sola lettura Scrivere un modulo Lua
AddMetricProvider<T>() Un contributo IMetricProvider, singleton, aggiunto al raccoglitore diagnostico Registrare un provider di metriche
AddConfig<T>(section) La [section] propria del plugin in config/moongate.toml, associata a T e registrata come singleton Aggiungere una sezione di configurazione
AddPersistenceAuth<T>() / AddPersistenceWorld<T>() Una facade di entità tipizzata per Accounts o Realm, con moduli gestiti internamente (richiede Moongate.Persistence) Persistenza PostgreSQL

priority conta solo per un servizio che implementa anche IMoongateStartupService: il bootstrap avvia i servizi per priorità crescente e li arresta al contrario, quindi un servizio prende una priorità più bassa dei servizi che ne dipendono. Valori incorporati:

Priorità Servizio
-1000 RedisConnectionService (ogni modalità)
-900 TimerWheelService
-800 IGameLoopService (GameLoopService)
-10 IUltimaDataService (UltimaDataService)
-5 IDataLoaderService (DataLoaderService; game e standalone)
-4 IMapService (MapService), IMultiService (MultiService); game e standalone, vedi File client e query sul mondo
-3 IStartingItemsService
0 (predefinito) ISessionService, IEventBusService, IPluginLoaderService, ICommandSystemService e ogni registrazione che omette priority
10 a 12 I servizi di gioco Ultima: IItemService, INpcService, IWorldPropsService (10); ILightService, IWeatherService, ISeasonService, IMusicService, IRegionAnnouncer, ItemDecayService (11); rigenerazione, fame, crimini, omicidi, combattimento, guardie, prigione, bacheche, pagine di aiuto, fedeltà dei pet (12) e altri
40 IWorldSaveService (WorldSaveService), IConnectionService
50 IPacketSendService
55 IBookAttachmentService
60 IPacketDispatchService
70 IScriptEngine (LuaScriptEngineService)
75 a 76 I servizi di script Lua che richiedono il motore collegato: NpcScriptService, IItemScriptService, IGumpScriptService, IEventScriptService, ISkillScriptService (75); IScheduleService, ISeasonalEventService, IItemTimerService (76)
100 IGameServerService
105 RedisRealmRegistrationService
110 IAdminApiService (AdminGrpcHostService, dal plugin di amministrazione; listener disabilitato per impostazione predefinita)
900 IDiagnosticService (DiagnosticService)
1000 IConsoleInputService (ConsoleInputService)

Un plugin che registra il proprio servizio di avvio sceglie una priorità relativa a questa tabella: dopo IGameLoopService (-800) se deve accodare lavoro al loop, dopo IScriptEngine (70) se richiede il motore già associato, e così via. I plugin si caricano prima dei controlli di persistenza, e la preparazione dello schema di persistenza termina prima di risolvere questo elenco di servizi di avvio, indipendentemente dalla priorità di un servizio del plugin.

RegisterPacketHandler<TPacket, THandler>() associa un singleton IPacketHandler<TPacket> a un tipo di pacchetto in ingresso, chiamato sincronicamente sul thread del game loop; un secondo gestore per lo stesso tipo genera un’eccezione prima dell’avvio. Un pacchetto definito dal plugin richiede anche RegisterIncomingPacket<TPacket>(), perché la sola registrazione del gestore non aggiunge il suo opcode al registro del protocollo; vedi Integrazione con l’host.

OnEvent<TEvent>(handler) si iscrive da Register al bus degli eventi posseduto dal container; il gestore viene atteso per ogni TEvent pubblicato finché vive il container.

Un plugin conserva le proprie impostazioni in una tabella propria di config/moongate.toml. Scrivi una classe con i valori predefiniti, implementa IConfigSection quando alcuni valori non sono consentiti, e aggiungila all’inizio di Register:

using Moongate.Server.Core.Extensions;
using Moongate.Server.Core.Interfaces.Config;
public sealed class GreeterConfig : IConfigSection
{
public string Greeting { get; set; } = "Welcome";
public GreeterLimits Limits { get; set; } = new(); // [greeter.limits]
public void Validate()
{
if (Limits.MaxPerMinute < 1)
{
throw new InvalidOperationException("greeter.limits.max_per_minute must be at least 1.");
}
}
}
public void Register(Container container)
{
var config = container.AddConfig<GreeterConfig>("greeter");
// Services can now take GreeterConfig in their constructor.
}
[greeter]
greeting = "Welcome"
[greeter.limits]
max_per_minute = 10
  • I nomi delle proprietà diventano chiavi snake_case; una classe annidata è una sottotabella.
  • Quando il file non ha la tabella [greeter], il plugin riceve i valori predefiniti e il server li aggiunge in fondo al file, così l’operatore vede ogni impostazione. Il resto del file non viene riscritto: commenti e ordine restano invariati. Se il file non può essere scritto, viene registrato un avviso e si usano i valori predefiniti.
  • Validate() viene eseguito dopo la lettura; un’eccezione interrompe l’avvio.
  • Un nome di sezione appartiene a un proprietario: le sezioni proprie del server (network, redis, persistence, …) e un nome già aggiunto da un altro plugin interrompono l’avvio con The configuration section [name] is already owned by the server or by another plugin.
  • Una chiave con il nome della sezione che non è una tabella (greeter = 5, [[greeter]]) interrompe l’avvio: aggiungere [greeter] accanto definirebbe la chiave due volte.
  • Le impostazioni vengono lette una volta, all’avvio; non c’è ricaricamento.

Come funziona. All’avvio il server legge config/moongate.toml una volta, prende le proprie sezioni e registra il file analizzato come ServerConfigDocument prima dell’esecuzione di qualsiasi Register di plugin. AddConfig rivendica il nome in quel documento, così due proprietari non lo condividono, poi legge la tabella o scrive i valori predefiniti. Questa scrittura della configurazione gestita dall’host è consentita in Register; non autorizza I/O su file del plugin, lavoro in background, socket o chiamate al database. Tutto avviene durante la registrazione, prima dell’avvio di qualsiasi servizio: un valore errato non raggiunge mai un server in esecuzione.

Usarla in un servizio. La sezione è un normale singleton, quindi un servizio la richiede nel costruttore:

public sealed class GreeterService
{
private readonly GreeterConfig _config;
public GreeterService(GreeterConfig config)
{
_config = config;
}
}

Una sottotabella che il plugin vuole distribuire separatamente viene registrata con container.RegisterInstance(config.Limits); il plugin Ultima lo fa affinché i suoi servizi ricevano WorldConfig o CharactersConfig anziché l’intero UltimaConfig.

Testarla. Un test che chiama direttamente Register del plugin registra prima un ServerConfigDocument, come fa il server:

var path = Path.Combine(directory, "moongate.toml");
File.WriteAllText(path, "[greeter]\ngreeting = \"Hi\"\n");
container.RegisterInstance(new ServerConfigDocument(path, TomlSerializer.Deserialize<TomlTable>(File.ReadAllText(path))!, []));
new GreeterPlugin().Register(container);
Assert.Equal("Hi", container.Resolve<GreeterConfig>().Greeting);

Il plugin Ultima possiede [ultima] e il plugin Admin possiede [admin_api] in questo modo.

Eventi del ciclo di vita della persistenza

Section titled “Eventi del ciclo di vita della persistenza”

Iscriviti durante Register agli eventi di Moongate.Server.Core.Data.Events:

container.OnEvent<PersistenceReadyEvent>(async (_, cancellationToken) =>
{
var items = await container.Resolve<IDataAccess<Item>>()
.GetAllAsync(cancellationToken);
// Load plugin state before startup services begin.
});
container.OnEvent<PersistenceStoppedEvent>((_, _) =>
{
// Persistence is disposed. Release plugin bookkeeping; do not query or save.
return Task.CompletedTask;
});

Item rappresenta la tua entità registrata; IDataAccess<T> proviene da Moongate.Persistence.Interfaces e OnEvent<TEvent> da Moongate.Server.Core.Extensions.

Evento Tempistica e garanzie
PersistenceReadyEvent Registrazione dei plugin e inizializzazione della persistenza sono terminate con successo, compresi controlli di connessione, migrazioni e schema. La persistenza è utilizzabile; seguono servizi di avvio e MoongateStartedEvent.
PersistenceStoppedEvent Un proprietario inizializzato della persistenza è stato rilasciato con successo, dopo l’arresto dei servizi e MoongateStoppedEvent, ma prima del rilascio del container. Usa un token non annullabile affinché gli osservatori dell’arresto possano completare.

Entrambi sono senza payload, attesi ed emessi al massimo una volta per ciclo di vita del bootstrap. Nulla viene pubblicato quando l’host non ha registrazioni di persistenza o l’inizializzazione fallisce; un successivo errore di avvio pubblica comunque PersistenceStoppedEvent, un segnale di chiusura, non conferma di un salvataggio finale del mondo. I gestori vengono eseguiti nell’operazione del ciclo di vita, non sul game loop; non attendere mai StartAsync o StopAsync del bootstrap al loro interno. Il bus degli eventi registra e isola le eccezioni degli osservatori, quindi la validazione critica di avvio appartiene a un servizio di avvio.

Per i comandi incorporati (echo, help, script, console, uptime, version, sql_backup e account, più quelli del plugin Ultima), vedi Comandi del server. Un comando registrato con CommandSourceType.InGame si può digitare anche in gioco con un punto iniziale, come .shutdown: il plugin Ultima registra, tra gli altri, shutdown, save, broadcast, spawn, add, set, remove, hide, unhide, resurrect, weather, season, time, go, gmtools, jail, pages e moongate.

Un comando è una classe che implementa ICommandExecutor, registrata con RegisterCommand<T>(name, description, source, minimumAccountType, descriptionMessage) come nella classe del plugin sopra. descriptionMessage è facoltativo: l’id di un messaggio in data/messages mostrato da help nella lingua del server al posto di description; un plugin può usare id superiori a quelli usati da Moongate. Anche i testi del comando possono passare da ILocalizationService: accettalo come parametro facoltativo del costruttore e usa localization.Text(id, english, values), che usa il testo inglese se manca un messaggio. CommandContext fornisce Arguments (i token dopo il nome del comando), Print e PrintError (una riga di output ciascuno) e il CancellationToken dell’invocazione.

Un comando può implementare anche ICommandArgumentCompleter per completare gli argomenti con TAB nella console: GetArgumentCompletions(previousArguments) dà i valori che può assumere l’argomento in digitazione, dopo gli argomenti già digitati ([] per il primo), oppure nessuno. Gira sul thread della console, quindi restituisce liste fisse o nomi letti dal disco, mai stato di gioco; un’eccezione viene registrata e non completa nulla, e un valore vuoto o contenente uno spazio viene scartato, poiché il parser dei comandi lo separerebbe.

public IReadOnlyList<string> GetArgumentCompletions(IReadOnlyList<string> previousArguments)
{
return previousArguments.Count == 0 ? ["on", "off"] : [];
}

CommandSourceType è un enum [Flags] (InGame, Console), quindi un comando può servire entrambe le sorgenti combinandole con OR, come echo incorporato. AccountType è Regular, GameMaster, Administrator in ordine crescente; un’invocazione console è sempre trattata come Administrator, quindi il minimo conta solo per lo stesso comando raggiunto da InGame, dove viene controllato il tipo di account della sessione chiamante.

I comandi vengono eseguiti sul thread che ha chiamato il sistema dei comandi, mai sul thread del game loop. Un comando che legge o modifica stato di gioco deve accodare il proprio elemento di lavoro a IGameLoopService e attenderne l’esito prima di scrivere il risultato tramite CommandContext. Il comando incorporato script reload (src/Moongate.Server/Commands/ScriptCommand.cs) è il modello di riferimento. Il GreetCommand dell’esempio mostra la validazione degli argomenti, compreso il controllo Enum.IsDefined che impedisce a una stringa numerica come "7" di diventare un membro enum non definito.

Il plugin Ultima incorporato registra il comando account: account create <username> <password> [Regular|GameMaster|Administrator]. Il livello predefinito è Regular; il prompt interattivo maschera il token della password e il comando attende IAccountService.CreateAccountAsync prima di riportare un esito. La registrazione permette anche agli amministratori in gioco, che lo digitano con un punto iniziale; il sottocomando api-access rifiuta l’uso in gioco.

Un plugin pubblicato come pacchetto .nupkg si installa con mgctl plugin install, che ne controlla la versione, decomprime il suo bundle in <root>/plugins/<id>/ e sostituisce uno più vecchio; vedi Installare e gestire i plugin. Il resto di questa sezione descrive cosa deve contenere un bundle copiato a mano.

Compila il progetto del plugin in Release e copia il suo output, tutto sotto bin/Release/net10.0/ e non solo la DLL di ingresso, in <root>/plugins/<BundleName>/ sul server di destinazione. <root> viene risolto all’avvio da --root-directory, poi MOONGATE_ROOT; senza entrambi il server rifiuta l’avvio.

Un bundle è una directory sotto <root>/plugins/; il suo nome è anche quello che il loader si aspetta per l’assembly di ingresso. Un bundle in plugins/mymod/ deve contenere mymod.dll, il proprio mymod.deps.json e ogni dipendenza privata non già distribuita dall’host. Una cartella il cui nome inizia con un punto non è un bundle e viene ignorata (mgctl plugin install lavora in tali cartelle). I bundle si caricano in ordine ordinale dei nomi delle directory; l’ordine di registrazione dei plugin è deciso dall’ordinamento delle dipendenze, non dall’ordine di caricamento.

Ogni bundle riceve un AssemblyLoadContext rilasciabile. La sua regola per ogni assembly referenziato dal bundle:

  1. Moongate.Core, Moongate.Hosting, Moongate.Server.Core, Moongate.Persistence, Moongate.Persistence.Migrations, FreeSql, FreeSql.Provider.PostgreSQL e Npgsql si risolvono sempre dall’host, e versione, cultura e token di chiave pubblica referenziati dal plugin devono corrispondere esattamente alla copia dell’host, più vecchia o nuova che sia. Compila con la versione esatta del pacchetto distribuita dall’host di destinazione.
  2. Ogni altro assembly prova prima il contesto di caricamento dell’host. Tutto ciò che l’host distribuisce (altri assembly Moongate.*, Serilog, DryIoc, LuaCSharp e così via) si risolve lì purché la versione referenziata dal plugin non sia più nuova di quella dell’host. Una copia nel bundle di tale assembly è peso inutile, mai una sostituzione; un riferimento più nuovo è trattato come non trovato e, con ExcludeAssets="runtime", il bundle non si carica.
  3. L’assembly di ingresso di un altro bundle (plugins/X/X.dll), trovato dal suo nome di assembly, si risolve nella copia che quel bundle ha caricato, così i due bundle condividono i suoi tipi. Un bundle non si risolve mai in sé stesso, e un nome di assembly portato da due bundle non viene condiviso. Vedi Dipendere da un altro plugin.
  4. Solo un assembly assente sia dall’host sia dagli altri bundle passa al bundle: prima il resolver .deps.json, poi una .dll con lo stesso nome accanto all’assembly di ingresso. Due bundle con le proprie copie di una libreria ricevono due istanze separate del suo stato statico.

Un plugin che usa i tipi di un altro plugin (un’interfaccia che esso registra nel container, un record che pubblica) li condivide tramite l’assembly di ingresso di quel plugin:

  1. Referenzia il progetto o il pacchetto dell’altro plugin nel .csproj e tienilo fuori dal tuo bundle: Private="false" su un riferimento di progetto, ExcludeAssets="runtime" su un pacchetto. Una copia rimasta nel bundle viene ignorata, mai una sostituzione.
  2. Aggiungi new MoongatePluginDependencyData("<its id>", minimumVersion) a dependencies: la dipendenza si registra per prima e una dipendenza mancante o più vecchia impedisce l’avvio.
  3. Installa entrambi i bundle sotto plugins/.

Si condivide solo l’assembly di ingresso, quindi limita i tipi che passi agli altri plugin a quelli dell’host e a quelli di quell’assembly: una libreria privata inclusa dalla dipendenza resta privata, e un tipo che ne proviene sono due tipi diversi sui due lati. Un bundle compilato contro una versione dell’assembly condiviso più nuova di quella caricata viene rifiutato all’avvio (vedi sotto). minimumVersion si confronta con la Version dei Metadata del plugin, non con la versione dell’assembly: tienile allineate, come fa la Metadata.Version di un plugin letta dal proprio assembly.

Ogni errore impedisce l’avvio con una InvalidOperationException identificata. Il caricamento di un bundle racchiude qualsiasi errore come Failed to load plugin bundle '{path}'. con il problema originale come InnerException:

Messaggio Causa
The plugin entry assembly is missing. La directory del bundle non contiene una DLL con il proprio nome
The assembly contains no public concrete IMoongatePlugin types. Nessun tipo plugin nell’assembly di ingresso
Plugin '{type}' needs a public parameterless constructor. La classe del plugin non può essere istanziata
Required host persistence contract '{assembly}' is unavailable; private copies are not supported. Un assembly con identità controllata manca nell’host
Incompatible host persistence contract '{assembly}'; host provides '{identity}'. Private copies are not supported. Il plugin è stato compilato contro una versione diversa di un assembly con identità controllata
eccezione interna FileNotFoundException Una dipendenza più nuova della copia dell’host, senza copia privata nel bundle
Plugin bundle '{bundle}' needs {assembly} {version}, but the bundle of that name provides {version}. Un bundle è stato compilato contro una versione dell’assembly di ingresso di un altro bundle più nuova di quella installata

Il registro valida l’intero insieme prima di eseguire qualsiasi Register; un insieme rifiutato lascia intatti i plugin già registrati:

Messaggio Causa
Plugin ID '{id}' is already registered or duplicated. Due plugin condividono un ID, compreso un plugin su disco che collide con uno registrato
Plugin '{id}' requires missing plugin '{dependency}'. Una dipendenza nomina un ID non fornito da nulla
Plugin '{id}' requires '{dependency}' >= {minimum}; found {version}. Il plugin disponibile è più vecchio di minimumVersion
Plugin dependency cycle: first -> second -> first. Un ciclo nel grafo delle dipendenze, stampato come percorso che lo ha chiuso
Plugin '{id}' failed during registration. Il Register del plugin ha generato un’eccezione, compresa una sezione di configurazione non valida o già posseduta; l’eccezione è l’InnerException

Testa tramite il vero loader: distribuisci il bundle compilato sotto una directory temporanea plugins/<name>/, registra in un Container i servizi host necessari al plugin, aggiungi PluginLoaderService e avvia un MoongateServerBootstrap. Poi risolvi ciò che Register ha aggiunto ed esercitalo: chiama Lua tramite il game loop, esegui comandi tramite CommandSystemService, raccogli uno snapshot diagnostico. tests/Moongate.Tests/Integration/Plugins/SamplePluginTests.cs fa esattamente questo per l’esempio ed è il modello da copiare. Per unit test che saltano il disco, costruisci new MoongatePluginRegistry(container), chiama Register(plugin) con un IMoongatePlugin in memoria e risolvi ciò che ha aggiunto dallo stesso container.

  • EnableDynamicLoading mancante. Una dipendenza non distribuita dall’host non raggiunge mai il bundle; vedi Creare il progetto.
  • Distribuire assembly dell’host nel bundle. La copia dell’host prevale sempre, quindi un Moongate.*.dll incluso è peso inutile; vedi Deployment e caricamento.
  • Eseguire lavoro o I/O propri del plugin o avviare thread in Register. Qui sono consentiti solo gli helper di registrazione gestiti dall’host (incluso AddConfig); nulla può ancora avviarsi; vedi Il contratto.
  • Registrare un oggetto legato al loop e accedervi da un comando senza accodare il lavoro. I comandi girano sul thread del chiamante, mai sul game loop; vedi Comandi console.
  • Una classe [ScriptModule] con una dipendenza nel costruttore non registrata prima dell’avvio. Il motore risolve l’istanza solo quando si avvia; vedi Scrivere un modulo Lua.

MoongateAdminPlugin è registrato in Program.cs dopo il plugin Ultima obbligatorio. È distribuito con il server e non viene caricato da plugins/. Il listener HTTP/2 facoltativo usa la configurazione [admin_api] e i servizi esistenti posseduti dall’host. Vedi API di amministrazione.

Creato con ❤️ da Tom

Offrimi un caffè