Aller au contenu principal

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 rien à travers un reload

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.api
  • btcrenaud.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​

FonctionRenvoie
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.
Zéro mob vivant ne veut pas dire nettoyé

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ésultatSignification
DoneL'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.

ActionCe qu'elle faitRefused 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 aliveMobs compte 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.
  • leaveRun appelé depuis le thread serveur : le joueur est parti quand elle renvoie Done. Appelé depuis un autre thread : le départ s'exécute au tick suivant du thread serveur et Done veut dire accepté ; l'événement indique qu'il a eu lieu.
  • placeChest : Done veut dire accepté. Le coffre apparaît au tick suivant, sur le thread qui possède son bloc ; DungeonChestPlaceEvent indique 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.

ChampTypeSignification
runIdStringId unique de la partie ; il disparaît avec elle.
dungeonIdStringId de l'entrée dungeon_instance que la partie joue.
dungeonNameStringNom d'affichage de cette entrée, tel que l'auteur l'a écrit (peut être vide).
stateDungeonRunStateOù en est la partie dans sa vie.
membersList<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).
worldNameString?Le monde où la partie se joue ; null tant qu'il n'est pas prêt.
startedAtEpochMsLongQuand la place de la partie a été réservée, en millisecondes epoch.
activatedAtEpochMsLong?Quand les joueurs ont été admis ; null en préparation et pour une partie reprise après un reload.
deadlineEpochMsLong?Quand la partie doit se terminer ; null pour une partie sans limite de temps (ou pas encore active).
seedLong?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.
generatedBooleanPropriété calculée : true quand le donjon a été fait par un générateur (il a une seed).

DungeonRunState​

ValeurSignification
PREPARINGLa place est réservée et le monde se prépare ; les joueurs ne sont pas encore dedans.
ACTIVELes joueurs sont dedans et le donjon se joue.
ENDINGLa 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​

ValeurSignification
COMPLETEDLe donjon a été terminé (dungeon_complete).
TIMEOUTLa limite de temps du donjon est écoulée.
ABANDONEDTous les joueurs sont partis.
ADMINUn administrateur l'a fermée (dungeon_close, la commande de fermeture, DungeonApi.endRun).

DungeonDeparture​

ValeurSignification
SENT_HOMELe joueur est parti volontairement (l'action ou la commande de départ, DungeonApi.leaveRun) et est téléporté là d'où il venait.
DISCONNECTEDIl 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_WORLDIl 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énementAnnulableQuandChamps
DungeonRunPreLaunchEventOuiUn 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)
DungeonRunStartEventNonUne 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
DungeonRunEndEventNonUne 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
DungeonPlayerLeaveRunEventNonUn 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
DungeonRoomEnterEventNonUn 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?
DungeonRoomLeaveEventNonUn 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?
DungeonMobDeathEventNonUn 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
DungeonMobsClearedEventNonUn 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
DungeonChestPlaceEventNonUn 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)
DungeonChestOpenEventOuiUn 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 :

  • roomId est l'id de l'entrée dungeon_room pour un donjon fixe, et generated:<n> pour une salle d'un donjon généré, dont templateId est l'id du modèle de salle dont elle est issue (null pour un donjon fixe).
  • chestId dé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. chestEntryId est dans tous les cas l'id de l'entrée dungeon_loot_chest.
  • DungeonRunPreLaunchEvent n'a pas de DungeonRunView : la partie n'existe pas encore. Un lancement qui a passé cet événement peut encore échouer plus tard ; seul DungeonRunStartEvent dit qu'une partie a vraiment commencé. Un lancement qui a échoué avant que la partie devienne ACTIVE ne 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 snippet content.dungeon_launch.cancelled est 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 DungeonRunEndEvent par 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 DungeonEvent n'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 reload ne 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, DungeonRoomLeaveEvent arrive avant le DungeonPlayerLeaveRunEvent du même départ, et son run.members ne 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)"
}

Voir aussi​