Serenity

repository·main·Indexed 41 days ago

https://github.com/serenityjs/serenity

A high-performance Minecraft Bedrock Edition server software built with Rust and TypeScript. It provides a developer-friendly API for creating custom gameplay experiences via a plugin system, including support for custom block types, block traits, and custom commands with overloads and permissions.

Tokens
39K
Snippets
130
Records
168
Agent score
50%

What's inside Serenity

  1. What is Serenity?

    main

    Serenity is a Minecraft Bedrock Edition Server Software built using Rust and TypeScript. It provides APIs and tools for building Minecraft Bedrock servers, allowing developers to focus on gameplay experiences without managing underlying network or protocol details.

    Serenity uses a powerful plugin system where plugins can be written in TypeScript or JavaScript, accessing the same APIs used to build the core server.

  2. How event listening priority works in Serenity

    main

    Serenity provides three methods for listening to events, which differ in their execution priority and ability to intercept the event loop:

    1. before: The highest priority. These listeners run before any other listeners. A before listener can cancel the event loop for all subsequent listeners by returning false. Returning true allows the loop to continue.
    2. on: Medium priority. These run on the same process tick immediately after before listeners. They cannot be canceled.
    3. after: Lowest priority. These run on the next process tick after all other listeners have completed.

    Use before when you need to intercept or block actions (like chat moderation), on for immediate reactions to game state changes, and after for logic that should only occur once the current event processing cycle is fully finalized.

    // Example of canceling an event using 'before'
    serenity.before(WorldEvent.PlayerChat, ({ player, message }) => {
      if (message === "cancel") return false;
      return true;
    });
    
    // Example of immediate reaction using 'on'
    serenity.on(WorldEvent.PlayerPlaceBlock, ({ block, permutationBeingPlaced }) => {
      // Logic runs immediately after 'before' listeners
    });
    
    // Example of post-process reaction using 'after'
    serenity.on(WorldEvent.PlayerPlaceBlock, ({ block, permutationBeingPlaced }) => {
      // Logic runs on the next process tick
    });
  3. What are SerenityJS Plugins?

    main
    Plugins are used to create server-side modifications or extensions to the base SerenityJS server software. They provide developers with full control over the server's behavior. SerenityJS provides templates for both TypeScript and Java to facilitate plugin development.
  4. Create command overloads with arguments

    main

    Overloads allow a single command to support multiple different argument structures. You define them within the registry function using registry.overload(args, executor).

    Argument Definition: Arguments are defined as an object where keys are the argument names. The value is an array containing:

    1. The Argument Type (e.g., TargetEnum, CustomEnum, or a class extending CustomEnum).
    2. A boolean indicating if the argument is optional (false means it is required).

    Executor Context: The overload executor receives an object containing the results of the parsed arguments (e.g., { target, kit }).

    import { TargetEnum, World } from "@serenityjs/core";
    
    function register(world: World): void {
      world.commandPalette.register(
        "ping",
        "Ping the server to check if it's responsive.",
        (registry) => {
          registry.overload(
            {
              target: [TargetEnum, false] // Required argument
            },
            ({ target }) => {
              // target.result contains the parsed entities
              const value = target.result[0].isPlayer() 
                ? target.result[0].username 
                : target.result[0].type.identifier;
    
              return { message: `Pong, ${value}!` };
            }
          );
        },
        () => { return { message: "Pong!" }; }
      );
    }
  5. Define Block Permutations

    main

    Block permutations (also known as states) allow you to create variations of a single block type. Each permutation can have its own set of component properties that override the base block type's properties.

    Use the createPermutation method on a CustomBlockType instance. The state for a permutation can be a string, number, or boolean.

    // Create permutations with specific states
    const permutation1 = exampleBlockType.createPermutation({ powered: false });
    // Override base component for this permutation
    permutation1.components.setLightEmission(0);
    
    const permutation2 = exampleBlockType.createPermutation({ powered: true });
    // Override base component for this permutation
    permutation2.components.setLightEmission(15);
  6. Create a Custom Block Type

    main

    To create a custom block, instantiate the CustomBlockType class from @serenityjs/core. The constructor requires a unique block ID (e.g., "serenity:example_block") and an optional configuration object.

    Once created, you can configure the block's base properties using the components property. Components assigned to the base type are inherited by all permutations unless explicitly overridden. Common component operations include:

    • setLightEmission(level: number): Sets the light emission level.
    • getGeometry().setModelIdentifier(id: string): Sets the geometry model identifier.
    • getMaterialInstances().createMaterialInstance(name: string, options: object): Creates a material instance (e.g., for textures and render methods).
    • setIsInteractable(value: boolean): Determines if the block can be interacted with.
    import { CustomBlockType } from '@serenityjs/core';
    import { MaterialRenderMethod } from '@serenityjs/protocol';
    
    // 1. Define the base block type
    const exampleBlockType = new CustomBlockType("serenity:example_block", { solid: true });
    
    // 2. Configure base components
    exampleBlockType.components.setLightEmission(15);
    
    const geometry = exampleBlockType.components.getGeometry();
    geometry.setModelIdentifier("geometry.example_block");
    
    const materials = exampleBlockType.components.getMaterialInstances();
    materials.createMaterialInstance("*", {
      texture: "example_block",
      render_method: MaterialRenderMethod.AlphaTest
    });
    
    // 3. Make it interactable for traits
    exampleBlockType.components.setIsInteractable(true);
  7. Configure command properties and permissions

    main

    When registering a command, you can provide a registry function as the third argument. This function allows you to configure metadata and access control for the command via a registry object.

    Key properties available on the registry object:

    • permissions: An array of strings (e.g., ["example.ping"]) that restricts command execution to users with those specific permissions.
    • debug: A boolean. If set to true, the command appears blue in the in-game command list (purely visual/informational).
    • overload(args, executor): Used to define specific argument structures and their corresponding logic.
    import { World } from "@serenityjs/core";
    
    function register(world: World): void {
      world.commandPalette.register(
        "ping",
        "Ping the server to check if it's responsive.",
        (registry) => {
          registry.permissions = ["example.ping"];
          registry.debug = true;
        },
        () => {
          return { message: "Pong!" };
        }
      );
    }
  8. Create and Register Block Traits

    main

    Block traits define custom behaviors for blocks (e.g., what happens when a block is interacted with). Traits are reusable across different block types.

    To create a trait:

    1. Extend the BlockTrait class from @serenityjs/core.
    2. Define a unique public static readonly identifier.
    3. Implement behavior methods (e.g., onInteract()). Inside these methods, you can access the block instance via this.block to get or set states using getState(key) and setState(key, value).

    To use a trait:

    • Register it to the block type using blockType.registerTrait(TraitClass).
    • Register it to the world instance using world.blockPalette.registerTrait(TraitClass).
    import { BlockTrait } from '@serenityjs/core';
    
    class ExampleBlockTrait extends BlockTrait {
      public static readonly identifier = "serenity:example_block_trait";
    
      public onInteract(): void {
        // Access the state defined in a permutation
        const state = this.block.getState("powered");
    
        // Toggle the state
        this.block.setState("powered", !state);
      }
    }
    
    // Register with the block type
    exampleBlockType.registerTrait(ExampleBlockTrait);
  9. Set up Serenity for local development

    main

    To modify the core application, clone the Serenity repository and follow these steps. Serenity uses Yarn Workspaces, so you must have Yarn installed first.

    1. Initialize the project: Run yarn install in the root directory.
    2. Compile the project: Since the project is written in TypeScript, run yarn build to compile it to JavaScript.
    3. Start the server: Navigate to the devapp directory and run the development command.
    yarn install
    yarn build
    cd devapp
    yarn dev
  10. Create a new Plugin project

    main

    To start a new plugin project, use the official sample-plugin repository as a template.

    Prerequisites

    Setup Steps

    1. Access the sample-plugin repository.
    2. Use the repository to create a new repository based on the template.
    3. Specify your desired repository name (e.g., my-plugin).
    4. (Optional) Select Include all branches if you wish to include the JavaScript template from the sample-plugin.
    5. (Optional) Add a description for your plugin.