Salta ai contenuti

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.

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 = 0 prende la grafica del genitore;
  • hue viene ereditato quando non impostato; senza un valore in tutta la catena, usa 0;
  • rarity ha 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 / Equip
await factory.SaveAsync(coin); // the database gives it its serial

Create 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.

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, resistances e sounds vengono ereditati chiave per chiave: le voci del figlio sostituiscono quelle corrispondenti, le altre del genitore rimangono;
  • tags vengono uniti: le chiavi del figlio aggiungono e sostituiscono quelle del genitore;
  • equipment e loot, quando impostati, sostituiscono quelli del genitore; carve, quando non impostato, è quello del genitore, e carve = "" 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):

  1. rifiuta una posizione esterna alla mappa, prima di tutto, e un template che si risolve senza corpo e senza razza;
  2. crea il mobile e lo colloca;
  3. pubblica MobileBeforeSpawnEvent: un handler può modificare il mobile, anche spostarlo; il luogo viene poi controllato di nuovo rispetto alla mappa;
  4. in un’unica transazione salva il mobile (il suo seriale proviene dall’intervallo dei mobile), poi:
    • uno zaino (ultima.items.backpack_template) indossato sul layer Backpack, per ogni NPC;
    • ogni voce di equipaggiamento tramite IItemFactoryService: una voce con un gender viene 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’gold estratto, come pile di ultima.items.gold_template di 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;
  5. dopo il commit pubblica MobileMovedToWorldEvent (con il luogo in cui il mobile è stato salvato) e poi MobileAfterSpawnEvent, 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.

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.)

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).

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.

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.

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.

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);

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.

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

Offrimi un caffè