Skip to content

Basic Usage

Creating a Minigame

Firstly we will take a look at the Minigame class. This is the heart of all minigames that you create, it implements the bare-bones logic and has common utilities that are used in minigames.

You can implement your own minigame by extending the Minigame class:

kotlin
class ExampleMinigame(
    server: MinecraftServer,
    uuid: UUID
): Minigame(server, uuid, ID, ExamplePhase.entries) {
    companion object {
        val ID: Identifier = Identifier("modid", "example")
    }
}

Let's break down what's going on here. Each minigame has access to the MinecraftServer, and each minigame has a UUID which are both passed in the constructor. Each minigame also provides an id which identifies what type of minigame it is, and the complete set of phases, which we will have a look at in a moment.

Phases

Phases determine the logic for your minigame. Typically, your minigame will have multiple phases which will be cycled through as time passes in your minigame. For example, for a UHC minigame you might have a Grace phase where pvp is disabled, then an Active phase where pvp will be enabled, and where the border starts shrinking, and finally a DeathMatch phase where all players are teleported into an area to fight to the death.

Phases must be implemented as an enum which implements MinigamePhase:

kotlin
enum class ExamplePhase: MinigamePhase {
    Grace,
    Active,
    DeathMatch
}

Using an enum class automatically determines the order in which the phases should be progressed in. The Minigame constructor expects your enums entries, e.g. ExamplePhase.entries, which has them in their defined order.

Each phase is tied to a string id, which must be unique to your other phases. This is generated by default using the enum's name (converted to snake case), but you can override this if you want.

While phases are minigame agnostic, it's typical for each minigame to have their own set of phases, as the enum names typically differ to more properly describe the lifecycle of a minigame.

Implementing Logic

Logic is tied to phases through the MinigamePhaseManager, accessible with the phases field on your minigame. This can either be done with coroutines or with Routines, depending on whether your minigame also implements SerializableMinigame; you cannot mix the two.

We will be using coroutines for now, Routines are covered in the Serialization Section.

Each coroutine is set for a specific phase and starts running when the minigame enters that phase. When the coroutine returns, the minigame will automatically advance to the next phase, and if the minigame leaves the phase before the coroutine is finished then the coroutine is cancelled. If there is no next phase to advance to then the minigame simply stays where it is.

kotlin
class ExampleMinigame(
    server: MinecraftServer,
    uuid: UUID
): Minigame(server, uuid, ID, ExamplePhase.entries) {
    @Listener
    private fun onInitialize(event: MinigameInitializeEvent) {
        this.phases.coroutines[ExamplePhase.Grace] = this::runGraceLogic
        this.phases.coroutines[ExamplePhase.Active] = this::runActiveLogic
    }

    private suspend fun runGraceLogic() {
        try {
            this.settings.canPvp.set(false)
            delay(5.Minutes)
            this.chat.broadcast(Component.literal("The grace period is over!"))
        } finally {
            this.settings.canPvp.set(true)
        }
    }

    private suspend fun runActiveLogic() {
        this.scopes.current.register<PlayerDeathEvent> { (player) ->
            player.sendSystemMessage(Component.literal("You died!"))
        }
        awaitCancellation()
    }

    companion object {
        val ID: Identifier = Identifier("modid", "example")
    }
}

Because cancelling a coroutine unwinds it, any cleanup that must happen when the phase ends, whether the phase ran to completion or was cut short, belongs in a finally block; here pvp is re-enabled either way. This is covered further in the Scheduling Section.

In the above example you can see that runActiveLogic never returns, and instead suspends indefinitely until cancelled with awaitCancellation (which happens on phase change or minigame close). This is how you write a phase which shouldn't automatically advance, and instead relies on other logic to advance its phase. It also registers an event listener in a scope which is closed as soon as the phase changes, we'll look at this in the Events Section.

We register our phase logic in an initializer event listener. Some logic we may not want to do in the constructor of our minigame, but instead later just before we start using our minigame. We'll have a deeper look into events later, but for now we can just use the @Listener annotation as above.

Changing Phases

The minigame advances to the next phase automatically whenever a phase's coroutine returns, but you can also change the phase yourself:

kotlin
val minigame: ExampleMinigame = // ...

minigame.phases.set(ExamplePhase.DeathMatch)

The minigame must be playing for this to work, and the phase must be one of the phases the minigame was constructed with.

You can query the phase the minigame is currently in through the minigame's state:

kotlin
val minigame: ExampleMinigame = // ...

// The current phase, null if the minigame isn't playing
val phase: MinigamePhase? = minigame.phaseOrNull

// Whether the minigame is currently in the Grace phase
val isGrace: Boolean = minigame.state.isAt(ExamplePhase.Grace)

// Whether the minigame has reached (or passed) the Active phase
val isAtLeastActive: Boolean = minigame.state >= ExamplePhase.Active

Minigame Lifecycle

A minigame's lifecycle is as follows: Created -> Ready -> Playing -> Closed. The state transitions are strictly forward moving and states should never be able to transition backwards.

A minigame is Created as soon as you construct it, at which point you can start adding players and configuring it. Calling start initializes the minigame (if it hasn't been initialized already with tryInitialize), moves it into the Playing state, and enters the first phase:

kotlin
val minigame: ExampleMinigame = // ...

minigame.start()

While playing, a minigame can be paused and unpaused; while paused, none of the minigame's scheduled tasks or coroutines will run:

kotlin
minigame.pause()
minigame.unpause()

Finally, a minigame is closed. This removes all players from the minigame, cancels all tasks, and unregisters all events; after a minigame has been closed no more players are permitted to join. If your minigame has finished naturally you should call complete instead, which does the same but additionally marks the minigame as having completed:

kotlin
minigame.complete()

// Or, if the minigame did not finish naturally
minigame.close()

Registering a Minigame

Now we have everything set up we can register our minigame, so we can run it on the server! We need to create a minigame factory which can generate instances of our minigame.

As our minigame is basic, we can just create an object singleton factory as we don't have any constructor arguments.

kotlin
object ExampleMinigameFactory: MinigameFactory {
    private val CODEC = MapCodec.unit(this)

    override fun create(context: MinigameCreationContext): Minigame {
        return ExampleMinigame(context.server, context.uuid)
    }

    override fun codec(): MapCodec<out MinigameFactory> {
        return CODEC
    }
}

We can then register our factory in our ModInitializier:

kotlin
object ExampleMinigameMod: ModInitializer {
    override fun onInitialize() {
        Registry.register(
            MinigameRegistries.MINIGAME_FACTORY,
            ExampleMinigame.ID,
            ExampleMinigameFactory.codec()
        )
    }
}

A factory is what lets the /minigame command create instances of your minigame, and it's also required if your minigame is serializable, see the Serialization Section.

Alternatively you can programmatically create your minigame:

kotlin
val server: MinecraftServer = // ...
val minigame = ExampleMinigame(server, UUID.randomUUID())
for (player in server.players) {
    minigame.players.add(player)
}
minigame.start()

Now we have registered our minigame factory we can hop in-game.

/minigame command

The minigame command lets you control all aspects of minigames in Arcade.

As we cover in detail more features of minigames, more of these commands will become useful, but it is placed here for ease of reference.

The first thing to note is that you must be an operator with a permission level of 4 to run this command.

NOTE

<minigame-id> can be specified by the uuid of the minigame, or by the id of the minigame (given that there is only one instance), or by - which refers to the minigame of the player executing the command.

  • /minigame list This lists all minigame instances.

  • /minigame create <factory-id> <data?> This allows you to create a minigame instance using a registered minigame factory. The data argument is optional and is the JSON passed to the factory's codec.

  • /minigame join <minigame-id> <player(s)?> This allows you to add players to a minigame instance.

  • /minigame leave <player(s)?> This allows you to remove players from the minigame they are in.

  • /minigame start <minigame-id> This starts a minigame.

  • /minigame close <minigame-id> This closes the minigame.

  • /minigame info <minigame-id> <path?> This gets information about the state of the minigame. The path argument is optional, if not specified, all the info will be displayed. You can specify an NBT path if you are only interested in that specific property, e.g. /minigame info - teams.eliminated.

  • /minigame team <minigame-id> spectators set <team> This sets the spectator team which all minigame spectators will join.

  • /minigame team <minigame-id> admins set <team> This sets the admin team which minigame admins will join.

  • /minigame team <minigame-id> eliminated add <team> This marks a team as being eliminated.

  • /minigame team <minigame-id> eliminated remove <team> This un-marks a team as being eliminated.

  • /minigame chat <minigame-id> spies add <player(s)?> This adds the specified player(s), or the player executing the command if not specified, to be a chat spy. This will make it so this player will see all chat messages (admin, spectator, and team chats).

  • /minigame chat <minigame-id> spies remove <player(s)?> This removes the specified player(s) from being a chat spy.

  • /minigame chat <minigame-id> mute <player(s)> This mutes the specified player(s).

  • /minigame chat <minigame-id> unmute <player(s)> This unmutes the specified player(s).

  • /minigame chat <minigame-id> announce <announcement> <title?> This broadcasts an announcement to everyone in the minigame.

  • /minigame spectating <minigame-id> add <player(s)?> This marks the player(s) as being a spectator.

  • /minigame spectating <minigame-id> remove <player(s)?> This un-marks the player(s) as being spectators.

  • /minigame admin <minigame-id> add <player(s)?> This makes the specified player(s) an admin.

  • /minigame admin <minigame-id> remove <player(s)?> This removes the specified player(s) from being an admin.

  • /minigame settings <minigame-id> This opens up the minigame's setting GUI.

  • /minigame settings <minigame-id> <setting> This gets the value of the specified setting.

  • /minigame settings <minigame-id> <setting> set from option <option> This sets the value of a setting from one of the pre-defined setting options.

  • /minigame settings <minigame-id> <setting> set from value <value> This sets the value of a setting to any value specified (this is JSON).

  • /minigame advancement <minigame-id> <modifier> only <advancement> <player(s)> This modifies one of the minigame's advancements for the specified player(s).

  • /minigame advancement <minigame-id> <modifier> all <player(s)> This modifies all the minigame's advancements for the specified player(s).

  • /minigame recipe <minigame-id> <modifier> only <recipe> <player(s)> This modifies one of the minigame's recipes for the specified player(s).

  • /minigame recipe <minigame-id> <modifier> all <player(s)> This modifies all the minigame's recipes for the specified player(s).

  • /minigame tags <minigame-id> <player> add <tag> This adds a minigame tag to the specified player.

  • /minigame tags <minigame-id> <player> remove <tag> This removes a minigame tag from the specified player.

  • /minigame tags <minigame-id> <player> list This lists all the minigame tags the specified player has.

  • /minigame phase <minigame-id> This gets the current phase the minigame is in.

  • /minigame phase <minigame-id> set <phase> This sets the current phase the minigame is in.

  • /minigame pause <minigame-id> This pauses the minigame.

  • /minigame unpause <minigame-id> This unpauses the minigame.

  • /minigame unpause <minigame-id> countdown <time?> <unit?> This starts a countdown that will unpause the minigame. You can optionally specify a time with a unit, if not specified it will default to 10 seconds.

  • /minigame unpause <minigame-id> ready <players|teams> This broadcasts a ready check for either all players or teams. Once all are ready, admins will be prompted to run the unpause countdown command.

  • /minigame tick <minigame-id> freeze This freezes ticking in the minigame.

  • /minigame tick <minigame-id> unfreeze This unfreezes ticking in the minigame.

  • /minigame tick <minigame-id> step <ticks?> This steps a frozen minigame forwards, by one tick if no time is specified.