Skip to content

A production-ready, scalable minigame network ecosystem for Minecraft, built on a decoupled, message-driven architecture.

Notifications You must be signed in to change notification settings

arnaudrmt/atlas

Repository files navigation

Atlas Banner

Java Spigot API BungeeCord API RabbitMQ License

A production-ready ecosystem for creating and managing scalable, instanced Minecraft minigames, built on a modern, decoupled, and self-healing architecture.


πŸš€ The Philosophy: Build Games, Not Infrastructure

The Atlas ecosystem is designed to solve the hardest problems of running a Minecraft network, server management, matchmaking, and performance, so that developers can focus purely on creating fun and unique game experiences. By leveraging a standalone orchestrator and a message-driven architecture, Atlas provides a robust and scalable foundation for any minigame network.


✨ Core Features

  • πŸš€ Dynamic Server Lifecycle: Game servers automatically register themselves on startup and gracefully deregister on shutdown. Stale or crashed servers are automatically culled by the Orchestrator.
  • 🌐 Asynchronous Matchmaking: A fully asynchronous, queue-based matchmaking system (/play <game>) ensures the network remains responsive under heavy load.
  • πŸ’‘ Zero-Config for Developers: Game developers focus purely on game logic. The engine handles all networking, server registration, and matchmaking automatically based on conventions.
  • βš™οΈ Resilient & Self-Healing: If the Orchestrator restarts, game servers detect they are unknown and will automatically re-register themselves, healing the network state.
  • 🧱 Robust State Machine: Built on the State Pattern, ensuring clean separation of logic for Lobby, Countdown, In-Game, and Post-Game states.
  • 🀝 Integrated Team System: A simple, fluent API for creating teams, auto-balancing players, and managing friendly fire via automatic Bukkit Scoreboard integration.
  • πŸ—ΊοΈ Performant World Management: Asynchronous loading of random maps from a template directory, world protection, and efficient, file-based map resetting for fast game turnover.
  • πŸ”§ Flexible Configuration: A two-tiered config system allows developers to define core game rules in a main config.yml and map-specific spawns and properties in map.yml files.

πŸ”¬ How It Works: A Technical Deep Dive

The Atlas ecosystem operates on an event-driven, message-based architecture that ensures scalability and resilience. Below is a step-by-step breakdown of the two primary workflows.

1. Server Startup & Registration

This flow describes how a new game server comes online and makes itself known to the entire network.

  1. Initiation (Nova Engine): A Spigot server starts. A developer's game plugin registers their Game object with the Nova Engine. The engine's State Machine transitions to the LobbyState.

  2. Message Creation (Nova Engine): The LobbyState.onEnter() method triggers the AtlasIntegrationService. It constructs a globally unique Server ID (e.g., spleef-game1-a4b9c1) and creates a ServerLifecycleMessage with the type REGISTER.

  3. Broadcast (RabbitMQ): The message is published to a fanout exchange named server_lifecycle. A fanout exchange broadcasts the message to all consumers subscribed to it.

  4. Reception (AtlasLink & Orchestrator): Both AtlasLink (on BungeeCord) and the Atlas Orchestrator are subscribed to the server_lifecycle exchange. They both receive the exact same REGISTER message simultaneously.

  5. Action (AtlasLink): Upon receiving the message, AtlasLink dynamically adds the new server's information (including its unique ID and address) to the BungeeCord proxy's server list. Players can now be connected to it.

  6. Action (Orchestrator): The Orchestrator adds the new server to its internal, thread-safe map of active servers. It now considers this server available for matchmaking.

  7. Heartbeating (Nova Engine): The NovaEngine starts a periodic, asynchronous task to send its current player count and game state to the Orchestrator via an HTTP POST request, keeping its status up-to-date.

2. Player Matchmaking

This flow describes what happens when a player types /play. It follows a classic Remote Procedure Call (RPC) pattern over a message queue.

  1. Request (Nova Engine): A player on any Spigot server (e.g., a lobby) runs /play spleef. The MatchmakingService in the Nova Engine does two things: > * It creates a temporary, exclusive reply queue for this specific server instance.

    • It creates a MatchmakingRequest message containing the player's UUID, the requested game type (spleef), and the name of its private reply queue.
  2. Queuing (RabbitMQ): The request is published to a direct queue named join_requests. This is a work queue, meant to be consumed by one of the available Orchestrator instances.

  3. Processing (Orchestrator): The Orchestrator, the sole consumer of the join_requests queue, picks up the message. It runs its matchmaking algorithm: filtering its list of known servers by the requested game type, ensuring they are in a joinable state, and finding the one with the fewest players.

  4. Response (Orchestrator): The Orchestrator creates a MatchmakingResponse message containing the target server's unique ID (or null if no server was found). It then publishes this response not to the main queue, but directly to the replyToQueue specified in the original request.

  5. Reception (Nova Engine): The MatchmakingService on the lobby server, which has been listening on its private reply queue, receives the response.

  6. Action (Nova Engine): It finds the player by their UUID and uses the BungeeCord plugin messaging channel (Connect) to send the player to the target server ID. Because AtlasLink already knows about this server, the connection succeeds instantly.


πŸ”§ For Server Admins: Setup Guide

  1. Prerequisites: You need a running RabbitMQ server. The easiest way is with Docker:

    docker run -d -p 5672:5672 -p 15672:15672 rabbitmq:3-management
  2. Download Releases: Go to the Releases Page of this repository and download the latest JARs.

  3. Run the Atlas Orchestrator: This is a standalone application.

    java -jar atlas-orchestrator-1.0.0-all.jar
  4. Install AtlasLink: Place atlas-link-1.0.0.jar in your BungeeCord plugins/ folder.

  5. Install Nova Engine: Place nova-engine-1.0.0-ENGINE.jar in your Spigot server plugins/ folder. You will need one instance of this JAR for every Spigot server in the network.

  6. Install a Game: Place a game built with the engine (e.g., SpleefGame.jar) in the same Spigot folder.

  7. Configure Spigot: Set a unique server-name in each Spigot instance's server.properties file (e.g., game-1, lobby-main). This is crucial for traceability.


βš™οΈ Configuration

To connect the components, you must provide them with your RabbitMQ server details.

Atlas Orchestrator

The Orchestrator requires a config.json file to be present in the same directory where you run the JAR file.

  1. Create a file named config.json.
  2. Place it next to atlas-orchestrator-1.0.0-all.jar.
  3. Add the following content and fill in your details:
{
    "rabbitmq": {
        "host": "localhost",
        "port": 5672,
        "username": "guest",
        "password": "guest"
    }
}

AtlasLink & Nova Engine

The BungeeCord and Spigot plugins will generate their own config.yml files upon first run.

  1. Start your BungeeCord and Spigot servers once with the plugins installed to generate the default configuration files.
  2. Stop the servers.
  3. Navigate to plugins/AtlasLink/config.yml and plugins/NovaEngine/config.yml and fill in your RabbitMQ details.

File Location: plugins/AtlasLink/config.yml

# Configuration for AtlasLink
rabbitmq:
  host: 'localhost'
  port: 5672
  username: 'guest'
  password: 'guest'

File Location: plugins/NovaEngine/config.yml

# Configuration for the Nova Engine
rabbitmq:
  host: 'localhost'
  port: 5672
  username: 'guest'
  password: 'guest'

πŸ’» For Developers: Creating a Game with Nova Engine

Building a game is designed to be simple and intuitive. The engine handles all the complex backend integration for you.

1. Project Setup

Add the nova-engine.jar to your project's libs folder and configure your build.gradle.kts:

// build.gradle.kts
dependencies {
// Tell Gradle to use the engine for coding, but not to bundle it.
    compileOnly(files("libs/nova-engine-1.0.0-ENGINE.jar"))
    compileOnly("org.spigotmc:spigot-api:1.21.1-R0.1-SNAPSHOT")
}

Then, tell Spigot that your plugin depends on the engine in your plugin.yml:

# plugin.yml
name: MyAwesomeGame
main: com.mygame.MyPlugin
version: 1.0
api-version: '1.21'
# This is critical for ensuring correct load order!
depend: [NovaEngine]

2. Implement the Game Class

Extend the Game class to define your minigame's core rules and provide the engine with your custom game states.

package com.mygame;

import fr.arnaud.nova.api.Game;
import fr.arnaud.nova.api.state.GameState;
import fr.arnaud.nova.internal.state.EndState; // You can reuse the engine's default states!
import org.jetbrains.annotations.NotNull;
import org.bukkit.ChatColor;

public class MyAwesomeGame extends Game {

    @Override @NotNull
    public String getgame() { return "awesome"; }

    @Override
    public int getMinPlayers() { 
        // Read from your own config.yml for flexibility!
        return getConfig().getInt("game-settings.min-players-to-start", 2);
    }

    @Override
    public int getMaxPlayers() { return 8; }

    // Factory method to provide your custom in-game logic.
    @Override @NotNull
    public GameState getInGameState() {
        return new MyAwesomeGameState(this);
    }
    
    // Factory method for the end state. We can reuse the default one!
    @Override @NotNull
    public GameState getEndState() {
        return new EndState(this);
    }

    @Override
    public void onPostLoad() {
        // This is a great place to set up teams.
        createTeam("hunters", "Hunters", ChatColor.RED).setMaxSize(2);
        createTeam("runners", "Runners", ChatColor.BLUE).setMaxSize(6);
    }
}

3. Implement a Custom GameState

Extend the abstract GameState class to define what happens during a specific phase of your game.

package com.mygame;

import fr.arnaud.nova.api.state.GameState;
import fr.arnaud.nova.api.Game;
import fr.arnaud.nova.core.NovaPlugin;
import org.bukkit.event.EventHandler;
import org.bukkit.event.entity.EntityDamageByEntityEvent;

public class MyAwesomeGameState extends GameState {

    public MyAwesomeGameState(Game game) {
        super(game);
    }

    @Override
    public void onEnter() {
        // Game is starting! Disable world protection.
        NovaPlugin.getInstance().getEngine().getWorldManager().setProtection(false);
        // Teleport players, give items, etc.
    }
    
    @Override
    public void onExit() {
        // The game is over, re-enable protection as a failsafe.
        NovaPlugin.getInstance().getEngine().getWorldManager().setProtection(true);
    }

    @EventHandler
    public void onPlayerDamage(EntityDamageByEntityEvent event) {
        // Your custom PvP logic here.
        // If a win condition is met...
        if (/* someone won */) {
            // Tell the engine to transition to the end state.
            NovaPlugin.getInstance().getEngine().getGameManager().transitionTo(game.getEndState());
        }
    }

    // Implement other required methods (onTick, onPlayerJoin, onPlayerQuit, getEnum)...
}

4. Register Your Game

Finally, in your main plugin class, tell the Nova Engine about your game.

package com.mygame;

import fr.arnaud.nova.core.NovaPlugin;
import org.bukkit.plugin.java.JavaPlugin;

public class MyPlugin extends JavaPlugin {
    
    @Override
    public void onEnable() {
    // Pass 'this' so the engine can load your config.yml
    NovaPlugin.getInstance().getEngine().getGameManager()
        .registerGame(new MyAwesomeGame(), this);
    }
}

That's it! Your minigame is now a fully integrated part of the Atlas network.

Diving Deeper: Configuring Maps & Teams

Beyond the basics, the Nova Engine provides powerful, convention-based systems for managing game maps and teams. Here’s how to use them.

Setting Up Game Maps

The engine automatically discovers, selects, and loads maps for your game. All you need to do is follow a simple folder structure.

1. Create the Folder Structure

Inside your Spigot server's folder, create the following directory structure. The engine will scan the <game> folder that matches the one returned by your Game class.

plugins/
└── NovaEngine/
└── maps/
β”œβ”€β”€ <game>/                    (e.g., "spleef")
β”‚   β”œβ”€β”€ <map_name_1>/          (e.g., "classic_arena")
β”‚   β”‚   β”œβ”€β”€ region/
β”‚   β”‚   β”œβ”€β”€ data/
β”‚   β”‚   └── map.yml            <-- Map-specific configuration
β”‚   └── <map_name_2>/          (e.g., "ice_rink")
β”‚       β”œβ”€β”€ ...
β”‚       └── map.yml
└── <another_game>/        (e.g., "skywars")
└── ...

2. Configure map.yml

Each map folder must contain a map.yml file to define its properties and spawn points.

Example: plugins/NovaEngine/maps/spleef/classic_arena/map.yml

# The friendly name for broadcasts and scoreboards.
display-name: "Classic Arena"

# The location players are sent to in the lobby/waiting state.
# Format: "world,x,y,z,yaw,pitch". The 'world' is ignored and will be the active game world.
lobby-spawn: "world,0,100,0,90,0"

# A list of spawn points for each team.
# The team IDs ('red', 'blue') MUST match the IDs you create in your Game class.
team-spawns:
  red:
    - "world,-20,80,0,-90,0"
    - "world,-22,80,2,-90,0"
    - "world,-20,80,4,-90,0"
  blue:
    - "world,20,80,0,90,0"
    - "world,22,80,-2,90,0"
    - "world,20,80,-4,90,0"

3. Using Map Data in Your Game

The engine loads the map data before your game starts. You can access it through the WorldManager to teleport players.

// In your custom InGameState.java
import fr.arnaud.nova.api.world.GameMap;
import org.bukkit.Location;

@Override
public void onEnter() {
GameMap currentMap = NovaPlugin.getInstance().getEngine().getWorldManager().getCurrentMap();

    for (GamePlayer gamePlayer : NovaPlugin.getInstance().getEngine().getPlayerManager().getPlayers()) {
        GameTeam team = gamePlayer.getTeam();
        if (team != null) {
            // Get the list of spawns for this player's team.
            List<Location> teamSpawns = currentMap.getTeamSpawns().get(team.getId());
            if (teamSpawns != null && !teamSpawns.isEmpty()) {
                // Teleport the player to a random spawn point for their team.
                Location spawnPoint = teamSpawns.get(new Random().nextInt(teamSpawns.size()));
                gamePlayer.getBukkitPlayer().teleport(spawnPoint);
            }
        }
    }
}

Creating and Managing Teams

The engine provides a fluent API to create teams and automatically handles scoreboard integration (name tags, friendly fire).

1. Creating Teams

The best place to create teams is in your Game class's onPostLoad() method, which runs after the configuration is loaded but before players join.

// In your MyAwesomeGame.java
@Override
public void onPostLoad() {
    // The createTeam method returns the GameTeam object, allowing you to chain methods.
    createTeam("red", "Red Team", ChatColor.RED).setMaxSize(8);
    createTeam("blue", "Blue Team", ChatColor.BLUE).setMaxSize(8);
}

2. Assigning Players to Teams

When a player joins, you can either assign them to a team automatically or manually. This is typically done in the onPlayerJoinGame hook.

// In your MyAwesomeGame.java
@Override
public void onPlayerJoinGame(GamePlayer player) {
    // The engine will find the smallest available team and assign the player.
    autoAssignTeam(player);

    GameTeam team = player.getTeam();
    if (team != null) {
        player.getBukkitPlayer().sendMessage("You have joined the " + team.getColor() + team.getDisplayName());
    }
}

3. Using Team Data in Your Game

You can easily get a player's team to implement game logic like friendly fire checks.

// In your custom InGameState.java

@EventHandler
public void onPlayerDamage(EntityDamageByEntityEvent event) {
    if (event.getEntity() instanceof Player victim && event.getDamager() instanceof Player attacker) {
        GamePlayer gameVictim = NovaPlugin.getInstance().getEngine().getPlayerManager().getPlayer(victim);
        GamePlayer gameAttacker = NovaPlugin.getInstance().getEngine().getPlayerManager().getPlayer(attacker);

        if (gameVictim != null && gameAttacker != null && gameVictim.getTeam() != null) {
            // Check if players are on the same team.
            if (gameVictim.getTeam().equals(gameAttacker.getTeam())) {
                // Cancel friendly fire!
                event.setCancelled(true);
                attacker.sendMessage(ChatColor.RED + "You can't hurt your teammates!");
            }
        }
    }
}

About

A production-ready, scalable minigame network ecosystem for Minecraft, built on a decoupled, message-driven architecture.

Resources

Stars

Watchers

Forks

Packages

No packages published