Implementare caricatori TOML
Questo riferimento è per gli sviluppatori di plugin C#. Leggi prima Scrivere un plugin. Per creare oggetti, NPC e bottino in TOML, inizia da Creare contenuti TOML. I caricatori esistenti non richiedono modifiche C# per caricare nuovi file nelle loro cartelle.
Template di oggetto a runtime
Section titled “Template di oggetto a runtime”ItemTemplatesLoader legge ogni *.toml sotto templates/items/, comprese le
sottocartelle, quando il server di gioco si avvia e risolve base_id una volta sola:
- un campo che può rimanere non impostato (
name,layer,weight,amount,stackable, …) e non è impostato prende il valore del genitore, risalendo la catena; item_id = 0prende la grafica del genitore;hueviene ereditato quando non impostato; senza un valore in tutta la catena, usa 0;rarityha sempre un valore, quindi viene mantenuto quello del template;tags, quando impostato, sostituisce quello del genitore; non vengono uniti.
Il server si arresta quando un id è vuoto o usato due volte (il messaggio indica entrambi i file),
un base_id non indica alcun template, base_id crea un ciclo (a → b → a), oppure un template
risolto fallisce ItemTemplate.Validate().
IItemTemplateService fornisce i template risolti (TryGet, Get, Count; gli id
distinguono maiuscole e minuscole). IItemFactoryService crea oggetti da essi:
var coin = factory.Create("gold_coin", amount: 250);coin.PutInContainer(backpack.Id, layout.RandomGridPosition(backpack.ItemId)); // or PlaceOnGround / Equipawait factory.SaveAsync(coin); // the database gives it its serialCreate costruisce l’ItemEntity in memoria, senza seriale né posizione. I valori casuali del
template (hue, amount, rarity) vengono scelti una volta, lì, e memorizzati con
l’oggetto; nient’altro viene copiato dal template, quindi name, movable e
visibility rimangono null e si applicano i valori del template. Una quantità superiore a 1 su un oggetto
non impilabile (stackable, altrimenti il flag tiledata Generic) genera un’eccezione. Il salvataggio
rifiuta un oggetto senza posizione e assegna a un nuovo oggetto un seriale da
world.items_id_seq, dentro Serial.MinItem..MaxItem. SaveAsync(items) salva un
elenco in una transazione, in ordine (un contenitore prima del suo contenuto), e
SaveAsync(transaction, item) salva dentro una transazione aperta dal chiamante.
Template di mobile a runtime
Section titled “Template di mobile a runtime”MobileTemplatesLoader legge ogni *.toml sotto templates/mobiles/, comprese le
sottocartelle, dopo i template di oggetto e risolve base_id una volta sola, come fa GET di UOX3
(il figlio eredita tutto e sostituisce ciò che imposta):
- un campo non impostato prende il valore del genitore, risalendo la catena;
skills,resistancesesoundsvengono ereditati chiave per chiave: le voci del figlio sostituiscono quelle corrispondenti, le altre del genitore rimangono;tagsvengono uniti: le chiavi del figlio aggiungono e sostituiscono quelle del genitore;equipmenteloot, quando impostati, sostituiscono quelli del genitore;carve, quando non impostato, è quello del genitore, ecarve = ""non ne ha nessuno;- tutto ciò che viene ereditato viene copiato, mai condiviso tra template.
Il server si arresta quando un id è vuoto o usato due volte, un base_id non indica alcun template o
crea un ciclo, un template fallisce MobileTemplate.Validate(), un name_list non indica alcuna lista di
data/names.toml ({gender} richiede le liste male e female), un oggetto di equipaggiamento non è
un template di oggetto, un id loot non è una tabella del bottino oppure un id carve non è una tabella di smembramento. IMobileTemplateService fornisce i template risolti (TryGet, Get,
Count).
IMobileFactoryService crea NPC da essi:
var spawned = await mobiles.SpawnAsync("guard", MapType.Felucca, new Point3D(1602, 1591, 20));// spawned.Mobile is saved with its serial; spawned.Equipment is what it wears.Create(templateId) costruisce la MobileEntity in memoria e tira ogni valore casuale
una volta: genere (random è 50/50, non impostato è maschio), corpo (quello del template, altrimenti il corpo della razza per
il genere da races.toml), nome (quello del template, altrimenti uno da name_list, dove
{gender} sceglie la lista male o female), pelle, capelli e barba (dal template,
altrimenti dalla razza; hair = [] è calvo; le femmine non hanno barba), statistiche (non impostate sono 10), punti vita,
mana e stamina (che assumono per default forza, intelligenza e destrezza), armatura, resistenze, fama,
karma e abilità (punti del template × 10, i decimi memorizzati dal mobile). title rimane null: si applica
quello del template. notoriety è quello del template, e per un template che non ne ha un umano viene lasciato senza, che
viene letto come innocente, e qualsiasi altro è attackable (grigio), come animali e mostri di ModernUO. Agli NPC salvati
prima di questo comportamento viene assegnato il valore all’avvio del server e viene salvato con il mondo.
SpawnAsync(templateId, map, location):
- rifiuta una posizione esterna alla mappa, prima di tutto, e un template che si risolve senza corpo e senza razza;
- crea il mobile e lo colloca;
- pubblica
MobileBeforeSpawnEvent: un handler può modificare il mobile, anche spostarlo; il luogo viene poi controllato di nuovo rispetto alla mappa; - in un’unica transazione salva il mobile (il suo seriale proviene dall’intervallo dei mobile), poi:
- uno zaino (
ultima.items.backpack_template) indossato sul layerBackpack, per ogni NPC; - ogni voce di equipaggiamento tramite
IItemFactoryService: una voce con ungenderviene saltata per l’altro genere; l’oggetto viene indossato sul proprio layer (quello del template, altrimenti quello dei tiledata per una grafica indossabile); un oggetto senza layer, oppure con un layer occupato, va nello zaino, come fa UOX3; - l’
goldestratto, come pile diultima.items.gold_templatedi al massimo 65535, nello zaino; - un tiro di ogni tabella
loot(una tabella elencata due volte viene tirata due volte), nello zaino; - tutto nello zaino viene collocato in un punto casuale dentro i limiti di
containers.toml;
- uno zaino (
- dopo il commit pubblica
MobileMovedToWorldEvent(con il luogo in cui il mobile è stato salvato) e poiMobileAfterSpawnEvent, senza annullamento: il mobile è ormai salvato. Un errore prima del commit lascia il mobile in memoria senza seriale.
Il bus di eventi registra l’eccezione di un handler e prosegue, quindi un handler non può fermare o annullare uno spawn.
SaveAsync(mobile) salva un mobile che ha già il proprio seriale.
Tabelle del bottino a runtime
Section titled “Tabelle del bottino a runtime”LootTemplatesLoader legge ogni *.toml sotto templates/loots/, comprese le sottocartelle,
prima dei template di mobile. Il server si arresta quando un id è vuoto o usato due volte, una voce
imposta sia item_id sia loot_template_id, un item_id non è un template di oggetto, un
loot_template_id non indica alcuna tabella, un weight è inferiore a 1, un amount può produrre valori fuori dall’intervallo da 1
a 65535, le tabelle annidate creano cicli oppure il loot di un template di oggetto indica una tabella inesistente. Una tabella senza voci viene conservata, registrata come avviso e non produce
nulla: due tabelle UOX3 distribuite (randomwands, random_useless_junk) sono vuote nei
dati stessi di UOX3.
ILootService.Roll(tableId) sceglie una voce, in proporzione a weight, e restituisce
ciò che produce, costruito tramite IItemFactoryService senza seriale né posizione: nulla
per una voce senza entrambi gli id (blank di UOX3), una pila per un oggetto impilabile, quel numero di
oggetti separati altrimenti e, per una tabella annidata, amount tiri di essa: LOOTLIST=randomgems,2 tira randomgems due volte. (UOX3 stesso
cerca la voce annidata come lista di oggetti e non genera nulla; Moongate fa ciò che i dati
indicano chiaramente.)
Tabelle di smembramento a runtime
Section titled “Tabelle di smembramento a runtime”CarveTemplatesLoader legge ogni *.toml sotto templates/carves/, comprese le sottocartelle, dopo i template di
oggetto e prima dei template di mobile, che indicano le tabelle con carve. Il server si arresta quando un id è vuoto
o usato due volte, l’item di una voce non è un template di oggetto, un amount può produrre valori fuori da 1 a 65535, una chance è
fuori da 1 a 100 o un message è sotto 0. Una tabella senza voci viene conservata e non dà nulla. Cosa
significano le tabelle, e cosa accade a un cadavere, è in Smembrare i cadaveri. ICarveService fornisce le tabelle (TryGet) e
smembra i cadaveri (Carve).
Il contratto del caricatore
Section titled “Il contratto del caricatore”IDataLoader<TEntity>, in Moongate.Server.Ultima:
public interface IDataLoader<TEntity>{ Task InitializeAsync(CancellationToken cancellationToken = default);
Task<DataLoaderResult<TEntity>> LoadDataAsync(CancellationToken cancellationToken = default);}InitializeAsync prepara il caricatore, aprendo file o connessioni; LoadDataAsync
legge tutto e lo restituisce in un DataLoaderResult<TEntity>, la cui unica proprietà
è IReadOnlyList<TEntity> Entities. Un caricatore per template di oggetto enumererebbe
ogni file .toml nella propria sottodirectory di templates/ e deserializzerebbe ciascuno
con TomlUtils.
Registrare un caricatore
Section titled “Registrare un caricatore”Chiama AddUltimaDataLoader<TLoader, TEntity> da Register di un plugin, lo stesso
punto in cui vengono registrati servizi e provider di metriche:
container.AddUltimaDataLoader<ItemTemplatesLoader, ItemTemplate>(12);Il caricatore è un singleton, raggiungibile sia tramite il tipo concreto sia come
IDataLoader<TEntity>. La registrazione viene aggiunta all’elenco eseguito da
DataLoaderService all’avvio.
Eseguire i caricatori
Section titled “Eseguire i caricatori”DataLoaderService è un normale servizio di avvio con priorità -5, dopo
IUltimaDataService a -10, perché un caricatore che legge file MUL o UOP richiede il
percorso del client già configurato. Su StartAsync esegue ogni caricatore registrato in
ordine crescente di priorità e conserva ogni risultato sotto il suo tipo di entità:
IReadOnlyList<ItemTemplate> items = dataLoaderService.GetEntities<ItemTemplate>();Richiedere un tipo per cui non è stato registrato nulla genera subito un’eccezione, indicando il tipo: una
chiamata AddUltimaDataLoader mancante fallisce esplicitamente al primo uso, non con un elenco silenziosamente
vuoto. Anche senza caricatori registrati, StartAsync termina comunque.
Campi risolti casualmente
Section titled “Campi risolti casualmente”Un campo di template a volte è un valore fisso e a volte significa “scegline uno tra questi ogni
volta che un’entità viene creata da questo template”. EnumValueSpec<TEnum> copre entrambi
senza richiedere due campi nel tipo del template:
public readonly struct EnumValueSpec<TEnum> where TEnum : struct, Enum{ public bool IsRandom { get; }
public static EnumValueSpec<TEnum> FromValue(TEnum value); public static EnumValueSpec<TEnum> FromCandidates(IReadOnlyList<TEnum> candidates); public static EnumValueSpec<TEnum> Random();
public TEnum Resolve();}Resolve() viene chiamato nel punto di uso, quando un’entità viene creata dal
template, non durante il caricamento: un campo random_of produce un valore diverso
a ogni spawn. Estrae da Moongate.Core.Random.BuiltInRng, il generatore
usato dal resto del codice.
Come testo, le tre forme sono:
| TOML | Significato |
|---|---|
rarity = "common" |
Sempre Common |
rarity = "random_of" |
Qualsiasi membro dell’enum, scelto di nuovo a ogni Resolve() |
rarity = "random_of:rare,epic,legendary" |
Uno di questi tre esatti, scelto di nuovo a ogni Resolve() |
L’analisi dei nomi dei membri ignora maiuscole, minuscole e trattini bassi; la scrittura usa
snake_case minuscolo, quindi FromValue(ItemRarityType.Epic).ToString() è "epic", corrispondente a come
lo digita un autore, e ciò che viene scritto viene sempre riletto. Vedi
EnumValueSpec per ogni forma accettata ed errore. Un
campo lo dichiara tramite il proprio tipo, senza altro:
public EnumValueSpec<ItemRarityType> Rarity { get; set; } = EnumValueSpec<ItemRarityType>.FromValue(ItemRarityType.Common);Campi risolti in un nuovo numero
Section titled “Campi risolti in un nuovo numero”RangeValueSpec<T> è la controparte numerica, generica su INumber<T>:
public readonly struct RangeValueSpec<T> where T : struct, INumber<T>{ public bool IsRandom { get; }
public static RangeValueSpec<T> FromValue(T value); public static RangeValueSpec<T> FromRange(T min, T max);
public T Resolve();}Come testo, un numero senza virgolette (amount = 5) è fisso; un min-max tra virgolette (amount = "5-10")
sceglie un nuovo valore in quell’intervallo inclusivo a ogni Resolve(). È accettato anche un singolo
numero tra virgolette (amount = "5"). Scrivere un valore fisso produce un numero senza virgolette;
scrivere un intervallo produce la forma tra virgolette. Vedi
RangeValueSpec per ogni forma accettata ed errore.
public RangeValueSpec<int> Amount { get; set; } = RangeValueSpec<int>.FromValue(1);Le tinte hanno un proprio tipo, HueSpec, con la stessa forma testuale fissa o a intervallo e valori
esadecimali come "0x03EA-0x0422"; ItemTemplate.Hue lo usa. Vedi
HueSpec.
Registrare un convertitore TOML
Section titled “Registrare un convertitore TOML”EnumValueSpec<TEnum>, RangeValueSpec<T>, HueSpec, Serial, i tipi punto, Rectangle2D e DiceSpec
leggono e scrivono tramite convertitori che MoongateUltimaPlugin registra una volta con
TomlUtils.AddTomlConverter; gli enum come Visibility passano per la factory condivisa dei convertitori di enum.
Un template non richiede altro che il tipo del campo. Per le forme accettate e scritte
di ogni tipo, gli errori e come scrivere e registrare un tuo
convertitore, vedi Tipi di valore TOML.
Creato con ❤️ da Tom
