Un modulo di estensione per Matrix per giocare di ruolo via chat.
Questa è una grossa semplificazione di come funziona effettivamente un server Matrix perchè in realtà internamente è molto più complesso. Per più dettagli, andatevi a leggere la specifica completa...
Tutte le cose che avvengono su Matrix sono rappresentate attraverso events, che i client possono inviare in vari canali di comunicazione detti rooms.
Ogni event ha un type
, una stringa che determina come viene gestito dal server.
I type che iniziano con m.
, come m.room.name
o m.room.message
, sono riservati, e vengono definiti nella specifica di Matrix.
È possibile definire eventi personalizzati dandogli un namespace come i package Java, come one.ryg.matrix.dnd.mycustomevent
.
Ogni event ha un content, un payload JSON arbitrario, il cui significato dipende dal type
dell'event.
Inoltre, gli event possono avere una state key, una stringa che li distingue in una di queste due categorie:
- se non ha una state key, un evento è un message, e rappresenta qualcosa di transitorio e immediato come appunto un messaggio
- se ha una state key, un evento è uno state, e rappresenta una modifica permanente a un metadato di una stanza, come il suo titolo.
Una stringa vuota come state key rende comunque l'evento uno state; se non è specificata la state key, significa che è una stringa vuota
""
!
Uno state più recente sovrascrive lo state più vecchio.
Il server provvede a salvare tutti gli state della room in un oggetto con la seguente forma:
{
"tipoevento": {
"statekey": "payload",
"statekey2": {"altro payload": "ma più bello"}
},
"m.room.name": {
"": {
"name": "Garasauto"
}
},
"m.room.topic": {
"": {
"topic": "Luogo di culto del Garasautismo."
}
},
"m.room.member": {
"@steffo:ryg.one": {
"avatar_url": "...",
"displayname": "Steffo",
"membership": "join"
},
"@nemesis:ryg.one": {
"displayname": "Balulo",
"membership": "invite"
},
"@gimbaro:ryg.one": {
"displayname": "Gimbardo",
"membership": "ban",
"reason": "È un clown."
}
},
"tk.garasfunny.car": {
"@steffo:ryg.one": {
"type": "Subalu Baracca"
},
"@pesca:ryg.one": {
"type": "Macchina gialla"
}
}
}
Ogni room è identificata da un id namespaced casuale, nel formato !frSboFqpSCmLMieeVJ:ryg.one
, il cui namespace è il server Matrix che l'ha creata.
Anche le room hanno un type
, che al momento viene solo utilizzato dai client per cambiare come vengono visualizzate le room.
La specifica definisce solo due possibili type
per le room:
null
, per le chat"m.space"
, contenitori di altre room simili alle categorie di Telegram, ma che possono essere innestati
Il modulo RPG che vogliamo creare aggiunge i seguenti concetti a Matrix.
Una campagna è l'insieme di tutte le partite di un gioco di ruolo ambientate nello stesso universo narrativo.
La campagna è realizzata come una room di tipo m.space
.
Tutte le room della campagna dovranno avere uno state di tipo one.ryg.matrix.dnd.campaign.v1
, contenente un oggetto vuoto.
{}
Esso verrà usato dai client per il play-by-chat per filtrare le room: non si vuole che essi visualizzano tutti gli space in cui un utente si trova, ma solo quelli con questo state.
Il titolo della campagna sarà determinato dal contenuto dello state m.room.name
della room che la rappresenta.
{
"name": "Sarcofago dell'Annullamento"
}
La campagna offre un campo di testo "fisso" in cui possono essere inserite informazioni di ogni tipo, detto il journal, il cui testo è determinato dal contenuto dello state m.room.topic
.
{
"topic": "Campagna nella giungla.\n\nGiorno 1."
}
Nella room della campagna, potranno entrare diversi utenti, nei seguenti "ruoli":
- organizzatore
- game master
- giocatore
- spettatore
Alla creazione, nella room deve essere impostato lo state m.room.power_levels
per gestire le autorizzazioni degli utenti:
-
alla chiave
users
, si associa a ciascun utente il suo ruolo nella campagna- organizzatore:
100
- game master:
50
- giocatore:
10
- organizzatore:
-
alla chiave
users_default
, si definisce il livello di privilegi per gli utenti non specificati in precedenza, ovvero gli spettatori:- spettatore:
0
- spettatore:
-
alla chiave
events
, si specifica il livello di privilegi necessari per effettuare determinate azioni:- cambiare il titolo della campagna,
m.room.name
:100
(organizzatore) - modificare il journal della campagna,
m.room.topic
:50
(game master) - aggiungere nuove partizioni alla campagna,
m.space.child
:50
(game master)
- cambiare il titolo della campagna,
-
alla chiave
events_default
, si definisce il livello di privilegi necessario per inviare messaggi i cui tipi non sono tra quelli specificati in precedenza; essendo questa una stanzam.space
, in essa non devono essere inviati messaggi di alcun tipo, quindi:100
(organizzatore)
-
allo stesso modo, si imposta lo stesso valore alla chiave
state_default
, il livello di privilegi necessario per impostare stati i cui tipi non sono tra quelli specificati in precedenza:100
(organizzatore)
-
infine, si impostano i permessi di base relativi alla stanza:
- chiedere a utenti di unirsi alla campagna,
invite
:50
(game master) - eliminare messaggi e stati,
redact
:50
(game master) - rimuovere membri esistenti,
kick
:50
(game master) eban
:100
(organizzatore)
- chiedere a utenti di unirsi alla campagna,
{
"users": {
"@steffo:ryg.one": 100,
"@mallllco:ryg.one": 50,
"@dicebot:ryg.one": 50,
"@gimbaro:matrix.org": 50,
"@nemesis:ryg.one": 10,
"@cookie:ryg.one": 10,
"@tizio:example.org": 10
},
"users_default": 0,
"events": {
"m.room.name": 100,
"m.room.topic": 50,
"m.space.child": 50,
},
"events_default": 100,
"state_default": 50,
"ban": 50,
"kick": 50,
"redact": 50,
"invite": 0
}
Per invitare nuovi utenti alla campagna, un utente autorizzato dovrà chiamare il metodo POST /_matrix/client/v3/rooms/{roomId}/invite
sul server.
Perchè questi utenti si uniscano effettivamente alla stanza, dovranno inviare loro stessi uno state m.room.member
che cambi la loro stessa membership
a join
.
Una partizione è una chat relativa a una specifica campagna, i cui contenuti sono visibili solo a un sottoinsieme di utenti.
Le partizioni potrebbero essere usate per rappresentare gruppi di giocatori nella campagna, luoghi nell'universo di gioco, oppure scene narrative.
Ciascuna partizione è implementata come una room con tipo null
.
Ogni partizione ha un titolo, determinato dal contenuto dello state m.room.name
della room:
{
"name": "Chat segreta con Paltri e Balu"
}
Ogni partizione ha anch'essa un journal, il cui testo è determinato dal contenuto dello state m.room.topic
.
{
"topic": "Inventario di questo party:\n\n- un sasso\n- un altro sasso"
}
Perchè una room venga considerata parte di uno space, devono essere impostati due stati:
-
all'interno della room della campagna, uno stato
m.space.child
con il room id della partizione in questione come chiave:{ "via": [ "ryg.one", ], "canonical": true }
-
all'interno della room della partizione, uno stato
m.space.parent
con il room id della campagna in questione come chiave:{ "via": [ "ryg.one", ], "canonical": true }
Sì, questo significa che una partizione potrebbe appartenere a più campagne contemporaneamente, ma sinceramente non penso dovrebbe cambiare niente a livello di frontend...
Il modulo per il gioco di ruolo prevede messaggi aventi uno di quattro formati diversi:
- narrazione
- dialogo
- sistema
- offtopic
I messaggi di narrazione descrivono una scena, e non vengono pronunciati da nessun personaggio.
Si rappresentano con un messaggio del tipo m.room.message
, e un contenuto nella seguente forma:
{
"msgtype": "one.ryg.matrix.dnd.narration.v1",
"body": "C'è uno gnomo dietro al bancone."
}
I messaggi di dialogo rappresentano qualcosa di pronunciato da un personaggio nel mondo di gioco.
Si rappresentano con un messaggio del tipo m.room.message
, e un contenuto nella seguente forma:
{
"msgtype": "one.ryg.matrix.dnd.dialogue.v1",
"body": "Dural: \"Dove sono?!\"",
"dialogue": {
"character": "Dural",
"text": "Dove sono?!"
}
}
I client useranno il contenuto dell'oggetto dialogue
per visualizzare il messaggio; la stringa body
viene usata per il rendering delle notifiche e per compatibilità con client Matrix "standard".
I messaggi di sistema rappresentano qualcosa relativo alle meccaniche di gioco, come il risultato di un lancio di un dado.
Si rappresentano con un messaggio del tipo m.room.message
, e un contenuto nella seguente forma:
{
"msgtype": "m.notice",
"body": "1d20+1 = 20+1 = 21"
}
I messaggi offtopic permettono la conversazione tra giocatori al di fuori del mondo di gioco.
Si rappresentano con un messaggio del tipo m.room.message
, e un contenuto nella seguente forma:
{
"msgtype": "m.text",
"body": "POSSO GIOCARE ZARIL ONECRIT MORTE ISTANTANEA ZIC ZAC"
}
Per rendere conversare come un dato personaggio più comodo, i client devono poter tenere traccia dei personaggi all'interno di una scena.
Questo viene fatto con uno stato del tipo one.ryg.matrix.dnd.character.v1
, avente il nome del personaggio stesso come chiave, e un oggetto vuoto come payload.
{}
I membri della partizione funzionano esattamente come per le campagne.
Come per le campagne, alla creazione, nella room deve essere impostato lo state m.room.power_levels
per gestire le autorizzazioni degli utenti:
-
alla chiave
users
, si associa a ciascun utente il suo ruolo nella campagna- organizzatore:
100
- game master:
50
- giocatore:
10
- organizzatore:
-
alla chiave
users_default
, si definisce il livello di privilegi per gli utenti non specificati in precedenza, ovvero gli spettatori:- spettatore (o giocatore senza permesso di parlare nella partizione):
0
- spettatore (o giocatore senza permesso di parlare nella partizione):
-
alla chiave
events
, si specifica il livello di privilegi necessari per effettuare determinate azioni:- cambiare il titolo della partizione,
m.room.name
:100
(organizzatore) - modificare il journal della partizione,
m.room.topic
:50
(game master) - associare la partizione a una campagna,
m.space.parent
:50
(game master) - aggiungere nuovi personaggi alla partizione,
one.ryg.matrix.dnd.character.v1
:10
(giocatore)
- cambiare il titolo della partizione,
-
alla chiave
events_default
, si definisce il livello di privilegi necessario per inviare messaggi i cui tipi non sono tra quelli specificati in precedenza:10
(giocatore)
-
allo stesso modo, si imposta lo stesso valore alla chiave
state_default
, il livello di privilegi necessario per impostare stati i cui tipi non sono tra quelli specificati in precedenza:100
(organizzatore)
-
infine, si impostano i permessi di base relativi alla stanza:
- chiedere a utenti di unirsi alla partizione,
invite
:50
(game master) - eliminare messaggi e stati,
redact
:50
(game master) - rimuovere membri esistenti,
kick
:50
(game master) eban
:100
(organizzatore)
- chiedere a utenti di unirsi alla partizione,
{
"users": {
"@steffo:ryg.one": 100,
"@mallllco:ryg.one": 50,
"@dicebot:ryg.one": 50,
"@gimbaro:matrix.org": 50,
"@nemesis:ryg.one": 10,
"@cookie:ryg.one": 10,
"@tizio:example.org": 10
},
"users_default": 0,
"events": {
"m.room.name": 100,
"m.room.topic": 50,
"m.space.parent": 50,
"one.ryg.matrix.dnd.character.v1": 10,
"m.room.message": 10
},
"events_default": 100,
"state_default": 50,
"ban": 50,
"kick": 50,
"redact": 50,
"invite": 0
}
Una cosa più fancy sarebbe permettere anche agli spettatori (
0
) di inviare messaggi, ma aggiungendo un modo nel client per nascondere i messaggi di utenti con power level inferiore a giocatore (10
)... Eviterei, per questo prototipo.