API développeur
Dungeon expose une petite API pour que d'autres extensions Typewriter puissent lire les parties (runs) du serveur, agir dessus et réagir à ce qui s'y passe. Cette page s'adresse aux développeurs ; pour construire des donjons, vous n'en avez pas besoin.
Qui peut l'utiliser
Typewriter charge toutes les extensions dans un seul class loader qui lui est propre, recréé à chaque /tw reload. Les classes de l'API sont donc visibles pour les autres extensions Typewriter qui déclarent dependency("btcrenaud", "Dungeon"), et pas pour un plugin Bukkit classique.
Un plugin qui a besoin de données de donjon doit utiliser les placeholders et les entrées de Dungeon, ou passer par une petite extension Typewriter qui fait le pont.
Ne gardez ni DungeonRunView ni écouteur d'événements à travers un reload de l'extension Dungeon. Lisez une vue fraîche et réenregistrez vos écouteurs au chargement de votre extension.
Seuls deux paquets sont publics et stables :
btcrenaud.dungeons.apibtcrenaud.dungeons.api.event
Tout le reste est interne et peut changer dans n'importe quelle version sans préavis.
Mise en place
Dans le build.gradle.kts de votre extension, ajoutez Dungeon au classpath de compilation et déclarez-la comme dépendance Typewriter, comme Dungeon le fait elle-même pour une autre extension :
dependencies {
compileOnly(files("libs/TypeWriter-DungeonExtension.jar"))
}
typewriter {
namespace = "yournamespace"
extension {
name = "YourExtension"
// ...
dependencies {
dependency("btcrenaud", "Dungeon")
}
}
}
DungeonApi
DungeonApi est un object Kotlin. Toutes ses fonctions peuvent être appelées depuis n'importe quel thread.
Lectures
| Fonction | Renvoie |
|---|---|
runOf(player: UUID) | Le DungeonRunView de la partie où se trouve le joueur, quel que soit son état (une partie qui se termine liste encore ses membres jusqu'à sa libération), ou null. |
run(runId: String) | La partie runId, ou null si elle n'existe plus. Un id de partie est unique et jamais réutilisé. |
runs() | Toutes les parties du serveur à cet instant, sans ordre particulier. Chaque vue a son propre instant ; la liste n'est pas atomique entre les parties. |
runsOf(dungeonId: String) | Les parties du donjon dont l'id d'entrée dungeon_instance est dungeonId. Vide pour un donjon inconnu. |
isInRun(player: UUID) | Si le joueur est dans une partie en préparation ou active : la question que le donjon se pose avant de laisser quelqu'un lancer ou rejoindre une partie. false pour un joueur dont la partie se termine déjà. |
roomsOf(player: UUID) | Les ids des salles où se trouve le joueur, telles que le donjon les a vues en dernier : les ids d'entrées dungeon_room d'un donjon fixe, generated:<n> pour un donjon généré. Vide si le joueur n'est dans aucune partie ou dans une partie sans salles. |
aliveMobs(runId: String, mobEntryId: String? = null) | Le nombre de mobs du donjon vivants dans la partie, uniquement ceux créés par l'entrée dungeon_mob_spawn mobEntryId si elle est donnée. 0 pour une partie inconnue. |
Le plafond de mobs vivants peut en retenir certains, et juste après un reload les mobs sont recomptés chunk par chunk. Pour savoir qu'un site a été nettoyé, écoutez DungeonMobsClearedEvent.
Actions
Chaque action renvoie un DungeonActionResult :
| Résultat | Signification |
|---|---|
Done | L'action a été acceptée. Certaines actions se terminent à un tick ultérieur ; l'événement correspondant indique quand. |
NotFound(what) | Ce que l'appel désigne n'existe pas (ou plus) ; what dit quelle chose manque. |
Refused(reason) | L'appel désigne des choses réelles mais le donjon refuse de le faire maintenant. Rien n'a été modifié. |
Les textes de what et reason sont en anglais et destinés à un développeur qui lit un journal ; ils ne sont jamais montrés aux joueurs.
| Action | Ce qu'elle fait | Refused quand |
|---|---|---|
endRun(runId) | Termine la partie comme le ferait un administrateur (dungeon_close) : les joueurs sont renvoyés chez eux et le monde est libéré. La séquence de fin s'exécute à un tick ultérieur ; DungeonRunEndEvent (raison ADMIN) indique qu'elle a commencé. | La partie est encore en préparation (un lancement ne s'annule pas d'ici : écoutez plutôt DungeonRunPreLaunchEvent), ou un autre appel possède déjà sa fin : une partie ne se termine qu'une fois. NotFound("run") pour une partie inconnue. |
leaveRun(player) | Fait sortir le joueur de sa partie, comme l'action dungeon_leave : il est renvoyé là d'où il venait. S'il était le dernier, la partie se termine juste après. DungeonPlayerLeaveRunEvent indique qu'il est parti. | La partie est encore en préparation (le lancement possède ses membres tant qu'elle n'est pas active), ou déjà en fin (sa séquence de fin renvoie ses membres chez eux). NotFound si le joueur n'est dans aucune partie. |
placeChest(runId, chestEntryId) | Place le coffre de butin de l'entrée dungeon_loot_chest dans la partie, comme un Loot Chest Trigger mais sans son, particules ni message. Placer un coffre déjà placé ne change rien. | L'entrée n'est pas en mode manuel, elle appartient à un autre donjon que la partie, la partie n'est pas active, ou le monde de la partie n'est pas chargé. NotFound pour une partie inconnue ("run") ou une entrée inconnue ("loot chest entry"). |
Threads
- Les lectures sont sûres depuis n'importe quel thread : elles lisent un instantané immuable du registre, et
aliveMobscompte sans prendre de verrou. - Les actions sont aussi sûres depuis n'importe quel thread et n'attendent jamais le jeu : ce qui doit s'exécuter sur le thread serveur est confié aux planificateurs du serveur.
leaveRunappelé depuis le thread serveur : le joueur est parti quand elle renvoieDone. Appelé depuis un autre thread : le départ s'exécute au tick suivant du thread serveur etDoneveut dire accepté ; l'événement indique qu'il a eu lieu.placeChest:Doneveut dire accepté. Le coffre apparaît au tick suivant, sur le thread qui possède son bloc ;DungeonChestPlaceEventindique qu'il est posé. Il peut ne pas l'être (un bloc qui ne peut pas être remplacé), ce qui est consigné dans le journal.
DungeonRunView
Une partie figée à un instant. C'est une copie qui ne change jamais après sa création : on peut la conserver, la comparer et la lire depuis n'importe quel thread ; redemandez-en une fraîche à DungeonApi. Tous les champs viennent d'une seule lecture du registre, donc state et members sont toujours cohérents.
| Champ | Type | Signification |
|---|---|---|
runId | String | Id unique de la partie ; il disparaît avec elle. |
dungeonId | String | Id de l'entrée dungeon_instance que la partie joue. |
dungeonName | String | Nom d'affichage de cette entrée, tel que l'auteur l'a écrit (peut être vide). |
state | DungeonRunState | Où en est la partie dans sa vie. |
members | List<UUID> | Les joueurs de la partie dans l'ordre d'arrivée : le premier est le chef. En ligne ou non (une place retenue compte). |
worldName | String? | Le monde où la partie se joue ; null tant qu'il n'est pas prêt. |
startedAtEpochMs | Long | Quand la place de la partie a été réservée, en millisecondes epoch. |
activatedAtEpochMs | Long? | Quand les joueurs ont été admis ; null en préparation et pour une partie reprise après un reload. |
deadlineEpochMs | Long? | Quand la partie doit se terminer ; null pour une partie sans limite de temps (ou pas encore active). |
seed | Long? | La graine qui a fabriqué le donjon, seulement pour un donjon généré ; null pour un donjon fixe. La rejouer donne le même donjon. |
generated | Boolean | Propriété calculée : true quand le donjon a été fait par un générateur (il a une seed). |
DungeonRunState
| Valeur | Signification |
|---|---|
PREPARING | La place est réservée et le monde se prépare ; les joueurs ne sont pas encore dedans. |
ACTIVE | Les joueurs sont dedans et le donjon se joue. |
ENDING | La partie est finie ou en cours de fermeture : les membres sont renvoyés chez eux et le monde libéré. Couvre aussi l'instant entre la fin de cette séquence et la disparition de la partie, et un lancement qui a échoué après sa réservation. |
Cette énumération appartient à l'API et ne suit pas la machine à états interne : la plateforme peut ajouter un état interne sans casser votre when.
DungeonEndReason
| Valeur | Signification |
|---|---|
COMPLETED | Le donjon a été terminé (dungeon_complete). |
TIMEOUT | La limite de temps du donjon est écoulée. |
ABANDONED | Tous les joueurs sont partis. |
ADMIN | Un administrateur l'a fermée (dungeon_close, la commande de fermeture, DungeonApi.endRun). |
DungeonDeparture
| Valeur | Signification |
|---|---|
SENT_HOME | Le joueur est parti volontairement (l'action ou la commande de départ, DungeonApi.leaveRun) et est téléporté là d'où il venait. |
DISCONNECTED | Il s'est déconnecté (ou son délai de reconnexion est écoulé) : il sera ramené là d'où il venait à sa prochaine connexion. |
LEFT_THE_WORLD | Il a quitté le monde de la partie par ses propres moyens, il n'y a donc nulle part d'où le renvoyer. |
Événements
Tous les événements sont des événements Bukkit du paquet btcrenaud.dungeons.api.event. Écoutez l'événement concret dont vous avez besoin.
| Événement | Annulable | Quand | Champs |
|---|---|---|---|
DungeonRunPreLaunchEvent | Oui | Un groupe s'apprête à lancer un donjon : le lancement a passé ses propres vérifications (taille du groupe, critères, coût d'entrée...) et rien n'est encore réservé, pris ou créé. | dungeonId: String, members: List<UUID> (joueurs en ligne uniquement, dans l'ordre donné), forced: Boolean (un administrateur a forcé le lancement), seed: Long? (la graine demandée ; null quand le lancement tire la sienne, ou pour un donjon fixe) |
DungeonRunStartEvent | Non | Une partie a commencé : elle est ACTIVE et ses joueurs sont dedans, après l'exécution des déclencheurs onStart et des événements on_dungeon_start. | run |
DungeonRunEndEvent | Non | Une partie se termine : dès que la séquence de fin commence et que les statistiques sont versées, avant les déclencheurs onEnd, le nettoyage des coffres et des mobs et le retour des joueurs. | run (état ENDING, membres encore dedans à cet instant), reason: DungeonEndReason |
DungeonPlayerLeaveRunEvent | Non | Un joueur a quitté une partie, quelle que soit la manière. Juste après son retrait de la partie. | run (ne liste plus le joueur), player: UUID (peut être hors ligne), departure: DungeonDeparture |
DungeonRoomEnterEvent | Non | Un membre est entré dans une salle, même si le enterOnce de la salle a déjà consommé ses déclencheurs. | run, player: Player, roomId: String, templateId: String? |
DungeonRoomLeaveEvent | Non | Un membre a quitté une salle, même si leaveOnce a déjà consommé ses déclencheurs. Suit aussi le départ d'un joueur de la partie (ou de son monde) alors qu'il se trouvait dans une salle. | run, player: Player, roomId: String, templateId: String? |
DungeonMobDeathEvent | Non | Un mob du donjon est mort alors que sa partie était ACTIVE. Les morceaux d'un mob qui se divise (slimes) et les autres descendants ne déclenchent rien. | run, entity: LivingEntity, killer: Player? (null si autre chose l'a tué), mobEntryId: String |
DungeonMobsClearedEvent | Non | Un site a été nettoyé : son dernier mob de sa dernière vague est mort. Une fois par nettoyage, pas une fois par membre. | run, mobEntryId: String |
DungeonChestPlaceEvent | Non | Un coffre de butin a été placé, une fois par coffre, lors de son premier enregistrement. Un coffre repris après un /tw reload ne déclenche rien. | run, chestId: String, chestEntryId: String, location: Location (copie de la position du bloc) |
DungeonChestOpenEvent | Oui | Un membre s'apprête à ouvrir (ou déclencher) un coffre de butin, en cliquant sur son bloc ou son PNJ. | run, player: Player, chestId: String, chestEntryId: String, location: Location |
Précisions sur les champs :
roomIdest l'id de l'entréedungeon_roompour un donjon fixe, etgenerated:<n>pour une salle d'un donjon généré, donttemplateIdest l'id du modèle de salle dont elle est issue (nullpour un donjon fixe).chestIddésigne ce coffre précis dans la partie : l'id de l'entrée pour son emplacement principal,<entry>#<n>pour un emplacement supplémentaire,<entry>@<room>.<marker>pour le coffre d'un marqueur.chestEntryIdest dans tous les cas l'id de l'entréedungeon_loot_chest.DungeonRunPreLaunchEventn'a pas deDungeonRunView: la partie n'existe pas encore. Un lancement qui a passé cet événement peut encore échouer plus tard ; seulDungeonRunStartEventdit qu'une partie a vraiment commencé. Un lancement qui a échoué avant que la partie devienneACTIVEne déclenche aucun événement de fin.
Annuler
DungeonRunPreLaunchEvent: un lancement annulé est refusé sans laisser de trace (aucune place réservée, aucun coût prélevé, aucun monde créé). Les joueurs ne sont rien prévenus par défaut, car le snippetcontent.dungeon_launch.cancelledest vide : l'extension qui annule doit s'expliquer elle-même aux joueurs, ou le propriétaire du serveur remplit ce snippet. Un lancement depuis un lobby remet le groupe dans le lobby.DungeonChestOpenEvent: un clic annulé est consommé. Le coffre ne s'ouvre pas, rien n'est tiré, aucune action ne s'exécute, aucune statistique ne compte, et le coffre reste tel quel pour que le joueur puisse réessayer. Expliquez-lui vous-même pourquoi ; rien n'est dit par défaut.
Contrat commun à tous les événements
- Un événement est déclenché une fois par fait, jamais une fois par membre : un seul
DungeonRunEndEventpar partie, quel que soit son nombre de joueurs. - Il est déclenché sur le thread qui a produit le fait : le thread serveur sur Paper.
- Un écouteur qui jette une exception est consigné par le serveur puis ignoré ; il ne peut jamais casser une partie. Un écouteur ne doit pas non plus bloquer, car il s'exécute en ligne au milieu de la séquence propre de la partie.
- Écouter la classe de base
DungeonEventn'est pas supporté : Bukkit refuse de l'enregistrer. Écoutez les événements concrets. - Les événements décrivent le chargement courant de l'extension. Un
/tw reloadne déclenche aucun événement de fin ou de départ pour les parties qu'il reprend : elles sont réadoptées silencieusement et leurs faits suivants sont déclenchés normalement. - Quand un joueur quitte une partie alors qu'il se trouve dans une salle,
DungeonRoomLeaveEventarrive avant leDungeonPlayerLeaveRunEventdu même départ, et sonrun.membersne liste plus le joueur.
Exemples
Un écouteur qui annule un lancement sous une condition et prévient les joueurs :
import btcrenaud.dungeons.api.event.DungeonRunPreLaunchEvent
import net.kyori.adventure.text.Component
import org.bukkit.Bukkit
import org.bukkit.event.EventHandler
import org.bukkit.event.Listener
class LaunchGate(private val isMaintenance: () -> Boolean) : Listener {
@EventHandler
fun onPreLaunch(event: DungeonRunPreLaunchEvent) {
if (event.forced || !isMaintenance()) return
event.isCancelled = true
// Personne d'autre n'expliquera : le snippet par défaut est vide.
event.members
.mapNotNull { Bukkit.getPlayer(it) }
.forEach { it.sendMessage(Component.text("Les donjons sont fermés pour maintenance.")) }
}
}
Lire la partie d'un joueur :
import btcrenaud.dungeons.api.DungeonApi
import java.util.UUID
fun describeRun(player: UUID): String {
val run = DungeonApi.runOf(player) ?: return "Pas dans un donjon."
return "${run.dungeonName} (${run.state}) : ${run.members.size} joueur(s)"
}