├── docs ├── README.md ├── contributing │ ├── CONTRIBUTING.md │ └── creating-a-broker.md ├── assets │ └── images │ │ ├── error.png │ │ └── joining.png ├── kdocs.md ├── reference │ ├── custom-events.md │ ├── unpin-command.md │ ├── start-command.md │ ├── pin-command.md │ ├── status-command.md │ ├── commands.md │ ├── reconcile-command.md │ ├── stop-command.md │ ├── reconciliation.md │ ├── command-broker.md │ ├── remove-command.md │ ├── advanced-installation.md │ ├── jar-broker.md │ ├── brokers.md │ └── docker-broker.md ├── getting_started │ ├── index.md │ ├── velocity_configuration.md │ ├── impulse_configuration.md │ ├── prerequisites.md │ ├── installation.md │ ├── connecting_and_exploring.md │ ├── jar_broker.md │ └── docker_broker.md ├── SUMMARY.md └── licensing.md ├── .gitattributes ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties └── libs.versions.toml ├── app ├── src │ ├── main │ │ ├── templates │ │ │ └── club │ │ │ │ └── arson │ │ │ │ └── impulse │ │ │ │ └── BuildConstants.java │ │ └── kotlin │ │ │ └── club │ │ │ └── arson │ │ │ └── impulse │ │ │ ├── inject │ │ │ ├── modules │ │ │ │ ├── ConfigManagerModule.kt │ │ │ │ ├── ServerModule.kt │ │ │ │ ├── BaseModule.kt │ │ │ │ └── BrokerModule.kt │ │ │ └── providers │ │ │ │ ├── RegisteredServerProvider.kt │ │ │ │ ├── BrokerFactoryProvider.kt │ │ │ │ └── BrokerConfigProvider.kt │ │ │ ├── commands │ │ │ ├── ImpulseCommand.kt │ │ │ ├── StopServer.kt │ │ │ ├── StartServer.kt │ │ │ ├── RemoveServer.kt │ │ │ ├── ReconcileCommand.kt │ │ │ ├── PinServer.kt │ │ │ ├── UnpinServer.kt │ │ │ ├── GenericServerCommand.kt │ │ │ └── ServerStatus.kt │ │ │ ├── server │ │ │ ├── ServerBroker.kt │ │ │ └── ServerManager.kt │ │ │ ├── ServiceRegistry.kt │ │ │ ├── Impulse.kt │ │ │ └── PlayerLifecycleListener.kt │ └── test │ │ └── kotlin │ │ └── club │ │ └── arson │ │ └── impulse │ │ ├── inject │ │ ├── MockServerModule.kt │ │ ├── MockClassesProvider.kt │ │ ├── MockFactoriesProvider.kt │ │ └── TestModule.kt │ │ ├── ServerBrokerTest.kt │ │ ├── ServiceRegistryTest.kt │ │ └── commands │ │ └── GenericServerCommandTest.kt └── build.gradle.kts ├── book.toml ├── .codecov.yaml ├── .github ├── ISSUE_TEMPLATE │ ├── support-request.md │ ├── feature_request.md │ └── bug_report.md ├── workflows │ ├── gradle-publish.yml │ ├── ci.yml │ └── deploy-docs.yml └── dependabot.yml ├── gradle.properties ├── api ├── src │ └── main │ │ └── kotlin │ │ └── club │ │ └── arson │ │ └── impulse │ │ └── api │ │ ├── config │ │ ├── BrokerConfig.kt │ │ ├── Configuration.kt │ │ ├── ShutdownBehavior.kt │ │ ├── ServerConfig.kt │ │ ├── Messages.kt │ │ └── LifecycleSettings.kt │ │ ├── server │ │ ├── Status.kt │ │ ├── BrokerFactory.kt │ │ └── Broker.kt │ │ └── events │ │ ├── RegisterBrokerEvent.kt │ │ └── ConfigReloadEvent.kt └── build.gradle.kts ├── CONTRIBUTING.md ├── command-broker ├── build.gradle.kts └── src │ └── main │ └── kotlin │ └── club │ └── arson │ └── impulse │ └── commandbroker │ ├── CommandBrokerConfig.kt │ ├── JarBrokerConfig.kt │ ├── CommandBrokerFactory.kt │ ├── JarBroker.kt │ └── CommandBroker.kt ├── settings.gradle.kts ├── docker-broker ├── build.gradle.kts └── src │ └── main │ └── kotlin │ └── club │ └── arson │ └── impulse │ └── dockerbroker │ ├── DockerBrokerFactory.kt │ └── DockerServerConfig.kt ├── .gitignore ├── theme └── catppuccin-alerts.css ├── gradlew.bat ├── README.md └── gradlew /docs/README.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /docs/contributing/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ../../CONTRIBUTING.md -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.png filter=lfs diff=lfs merge=lfs -text 2 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Arson-Club/Impulse/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /docs/assets/images/error.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:c6073f2c388c44199010188d9f7e0a66acc2806e2feba2e641c8c6af65e771ad 3 | size 2251902 4 | -------------------------------------------------------------------------------- /docs/assets/images/joining.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:f0a6fd8431214bbd5f3ff6cee89101331cc5820842b86f2a719ec0de8dee3397 3 | size 2200736 4 | -------------------------------------------------------------------------------- /docs/kdocs.md: -------------------------------------------------------------------------------- 1 | # KDocs 2 | 3 | The generated KDocs for Impulse can be found [here](https://arson-club.github.io/Impulse/kdocs/index.html). This will 4 | give you a detailed look at the Impulse API and how to use it. -------------------------------------------------------------------------------- /app/src/main/templates/club/arson/impulse/BuildConstants.java: -------------------------------------------------------------------------------- 1 | package club.arson.impulse; 2 | 3 | // The constants are replaced before compilation 4 | public class BuildConstants { 5 | 6 | public static final String VERSION = "${version}"; 7 | } 8 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | authors = ["Dabb1e"] 3 | language = "en" 4 | multilingual = false 5 | src = "docs" 6 | title = "Impulse" 7 | 8 | [output.html] 9 | additional-css = ["./theme/catppuccin.css", "./theme/catppuccin-alerts.css"] 10 | default-theme = "latte" 11 | preferred-dark-theme = "mocha" 12 | 13 | [preprocessor.alerts] 14 | -------------------------------------------------------------------------------- /docs/reference/custom-events.md: -------------------------------------------------------------------------------- 1 | # Custom Events 2 | 3 | Impulse fires a few custom events that you can listen for in your own plugins. 4 | 5 | #### `ConfigReloadEvent` 6 | 7 | This is fired whenever Impulse reloads its configuration. You can listen for this event to inject your own logic into 8 | the 9 | resulting configuration. 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/kotlin/club/arson/impulse/inject/modules/ConfigManagerModule.kt: -------------------------------------------------------------------------------- 1 | package club.arson.impulse.inject.modules 2 | 3 | import com.google.inject.AbstractModule 4 | 5 | class ConfigManagerModule(private val reloadOnInit: Boolean) : AbstractModule() { 6 | override fun configure() { 7 | bind(Boolean::class.java).toInstance(reloadOnInit) 8 | } 9 | } -------------------------------------------------------------------------------- /docs/reference/unpin-command.md: -------------------------------------------------------------------------------- 1 | # unpin 2 | 3 | The `unpin` command is used to allow Impulse to stop or remove a server that was previously pinned. 4 | 5 | ``` 6 | impulse unpin 7 | ``` 8 | 9 | ### Specify a server 10 | 11 | The server argument is the name of the server you would like to start. This is the name you defined in the `servers` 12 | section of the `config.yaml` file. 13 | 14 | ### Examples 15 | 16 | ``` 17 | impulse unpin smp 18 | ``` 19 | 20 | ### Permission Scope 21 | 22 | Use the `impulse.server.unpin` permission scope to control who can use this command. 23 | -------------------------------------------------------------------------------- /.codecov.yaml: -------------------------------------------------------------------------------- 1 | component_management: 2 | default_rules: 3 | statuses: 4 | - type: project 5 | target: auto 6 | branches: 7 | - "!main" 8 | individual_components: 9 | - component_id: api 10 | name: API 11 | paths: 12 | - api/** 13 | - component_id: app 14 | name: App 15 | paths: 16 | - app/** 17 | - component_id: docker_broker 18 | name: Docker Broker 19 | paths: 20 | - docker-broker/** 21 | - component_id: command_broker 22 | name: Command Broker 23 | paths: 24 | - command-broker/** -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/support-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Support Request 3 | about: You are encountering an issue setting up Impulse and would like support 4 | title: "[SUPPORT]" 5 | labels: support 6 | assignees: dabb1e 7 | 8 | --- 9 | 10 | **Describe the Issue** 11 | A clear and concise description of what the bug is. 12 | 13 | **Your config.yaml:** 14 | ```yaml 15 | Attach or include your configuration 16 | MAKE SURE TO REMOVE ANY SENSITIVE VALUES 17 | ``` 18 | 19 | **Log Messages** 20 | If applicable, attach any log messages from Impulse. 21 | 22 | **Versions:** 23 | Which version of Impulse are you using? 24 | Which version of Velocity are you using? 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /docs/reference/start-command.md: -------------------------------------------------------------------------------- 1 | # start 2 | The start command is used to start a configured server without a client connecting. 3 | ``` 4 | impulse start 5 | ``` 6 | This is useful for scripts or times where you would like to a server to start and prepare things before batch connecting clients (for instance a minigame instance). 7 | 8 | ### Specify a server 9 | The server argument is the name of the server you would like to start. This is the name you defined in the `servers` section of the `config.yaml` file. 10 | 11 | ### Aliases 12 | - `impulse warm` 13 | 14 | ### Examples 15 | ``` 16 | impulse start smp 17 | ``` 18 | 19 | ### Permission Scope 20 | Use the `impulse.server.warm` permission scope to control who can use this command. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[FEATURE] New Feature Idea!" 5 | labels: enhancement 6 | assignees: dabb1e 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /docs/reference/pin-command.md: -------------------------------------------------------------------------------- 1 | # pin 2 | 3 | The `pin` command is used to ensure that a server is not stopped or removed by Impulse. 4 | 5 | ``` 6 | impulse pin 7 | ``` 8 | 9 | This is useful for servers that you want to prevent auto shutdown on temporarily. For long term static servers, use the 10 | `allowAutoStop` setting in the server configuration. 11 | 12 | ### Specify a server 13 | 14 | The server argument is the name of the server you would like to start. This is the name you defined in the `servers` 15 | section of the `config.yaml` file. 16 | 17 | ### Examples 18 | 19 | ``` 20 | impulse pin smp 21 | ``` 22 | 23 | ### Permission Scope 24 | 25 | Use the `impulse.server.pin` permission scope to control who can use this command. 26 | -------------------------------------------------------------------------------- /docs/reference/status-command.md: -------------------------------------------------------------------------------- 1 | # status 2 | The status command is used to check the status of a servers. 3 | ``` 4 | impulse status Optional 5 | ``` 6 | If not provided with a server name, this command will return the status of all servers. If a server name is provided, it will return the status of that server only. 7 | 8 | ### Specify a server 9 | The server argument is the name of the server you would like to check the status of. This is the name you defined in the `servers` section of the `config.yaml` file. 10 | 11 | ### Examples 12 | Get the status of all servers: 13 | ``` 14 | impulse status 15 | ``` 16 | 17 | Get the status of a specific server: 18 | ``` 19 | impulse status smp 20 | ``` 21 | 22 | ### Permission Scope 23 | Use the `impulse.server.status` permission scope to control who can use this command. 24 | -------------------------------------------------------------------------------- /app/src/test/kotlin/club/arson/impulse/inject/MockServerModule.kt: -------------------------------------------------------------------------------- 1 | package club.arson.impulse.inject 2 | 3 | import club.arson.impulse.api.config.ServerConfig 4 | import club.arson.impulse.api.server.Broker 5 | import com.google.inject.AbstractModule 6 | import com.google.inject.Provides 7 | import com.velocitypowered.api.proxy.server.RegisteredServer 8 | 9 | class MockServerModule( 10 | private val registeredServer: RegisteredServer, 11 | private val broker: Broker, 12 | private val config: ServerConfig, 13 | ) : AbstractModule() { 14 | override fun configure() { 15 | bind(object : com.google.inject.TypeLiteral() {}) 16 | .toInstance(registeredServer) 17 | } 18 | 19 | @Provides 20 | fun provideBroker(): Broker { 21 | return broker 22 | } 23 | 24 | @Provides 25 | fun provideConfig(): ServerConfig { 26 | return config 27 | } 28 | } -------------------------------------------------------------------------------- /docs/getting_started/index.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | Impulse is a relatively simple program to get started with. In this guide we will walk you through how to set up a 4 | simple survival multiplayer (SMP) server running on fabric. We will configure our main server to start on an initial 5 | user connection and to automatically stop after 5 minutes of inactivity. Below is an outline of what each section 6 | covers. If you are new to Impulse we recommend reading through the entire guide. 7 | 8 | 1. [Prerequisites](prerequisites.md) - Covers the software and configuration needed to get started with Impulse 9 | 2. [Installation](installation.md) - Covers how to install Impulse into your Velocity proxy 10 | 3. [Velocity Configuration](velocity_configuration.md) - Config needed in velocity 11 | 4. [Impulse Configuration](impulse_configuration.md) - How to configure Impulse to manage the server 12 | 5. [Connecting and Exploring](connecting_and_exploring.md) - A quick tour of the features Impulse adds to Velocity and 13 | your server -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # 2 | # Impulse Server Manager for Velocity 3 | # Copyright (c) 2025 Dabb1e 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU Affero General Public License as 7 | # published by the Free Software Foundation, either version 3 of the 8 | # License, or (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU Affero General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Affero General Public License 16 | # along with this program. If not, see . 17 | # 18 | version=0.3.3-SNAPSHOT 19 | org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled 20 | org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true 21 | org.gradle.caching=true 22 | org.gradle.configuration-cache=true -------------------------------------------------------------------------------- /docs/reference/commands.md: -------------------------------------------------------------------------------- 1 | # Commands 2 | 3 | Impulse provides several commands to help you manage your servers both in-game and in the console. The following is a 4 | list of each command and a brief description. For more information on a specific command, see its respective page. 5 | 6 | * [`impulse start `](start-command.md) -- Start a server if it is not already running. 7 | * [`impulse stop `](stop-command.md) -- Stop a server if it is running. 8 | * [`impulse remove `](remove-command.md) -- Stop a server and remove its underlying resources (excluding save 9 | data). 10 | * [`impulse reconcile `](reconcile-command.md) -- Reconcile a server's configuration with its current state. 11 | * [`impulse status Optional`](status-command.md) -- Get the status of a server or all servers. 12 | * [`impulse pin `](pin-command.md) -- Pin a server to prevent it from being stopped automatically. 13 | * [`impulse unpin `](unpin-command.md) -- Unpin a server to allow it to be stopped automatically. 14 | -------------------------------------------------------------------------------- /.github/workflows/gradle-publish.yml: -------------------------------------------------------------------------------- 1 | name: Gradle Package 2 | 3 | on: 4 | release: 5 | types: [ created, edited ] 6 | 7 | jobs: 8 | build: 9 | 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: read 13 | packages: write 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: Set up JDK 17 18 | uses: actions/setup-java@v4 19 | with: 20 | java-version: '17' 21 | distribution: 'temurin' 22 | server-id: github 23 | settings-path: ${{ github.workspace }} 24 | 25 | - name: Setup Gradle 26 | uses: gradle/actions/setup-gradle@af1da67850ed9a4cedd57bfd976089dd991e2582 # v4.0.0 27 | 28 | - name: Build with Gradle 29 | run: ./gradlew -Pversion=${{ github.ref_name }} build 30 | 31 | - name: Publish to GitHub Packages 32 | run: ./gradlew -Pversion=${{ github.ref_name }} publish 33 | env: 34 | GITHUB_ACTOR: ${{ github.actor }} 35 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 36 | -------------------------------------------------------------------------------- /api/src/main/kotlin/club/arson/impulse/api/config/BrokerConfig.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Impulse Server Manager for Velocity 3 | * Copyright (c) 2025 Dabb1e 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Affero General Public License as 7 | * published by the Free Software Foundation, either version 3 of the 8 | * License, or (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Affero General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Affero General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package club.arson.impulse.api.config 20 | 21 | @Target(AnnotationTarget.CLASS) 22 | @Retention(AnnotationRetention.RUNTIME) 23 | annotation class BrokerConfig(val brokerId: String) -------------------------------------------------------------------------------- /docs/reference/reconcile-command.md: -------------------------------------------------------------------------------- 1 | # reconcile 2 | The reconcile command is used to force a server to reload its configuration and trigger a restart if needed. 3 | ``` 4 | impulse reconcile 5 | ``` 6 | Although reconciliation normally happens automatically, this command can be helpful if you have `forceServerReconciliation` set to `false`. It will trigger the reconciliation immediately rather than waiting for a natural stop and start cycle. This is particularly helpful for long-lived lobbies. 7 | 8 | > [!NOTE] 9 | > The exact behavior of reconciliation is broker specific. In general, it will reload the configuration and restart or remove and recreate the server as needed. 10 | 11 | ### Specify a server 12 | The server argument is the name of the server you would like to reconcile. This is the name you defined in the `servers` section of the `config.yaml` file. 13 | 14 | ### Examples 15 | ``` 16 | impulse reconcile smp 17 | ``` 18 | 19 | ### Permission Scope 20 | Use the `impulse.server.reconcile` permission scope to control who can use this command. 21 | -------------------------------------------------------------------------------- /docs/reference/stop-command.md: -------------------------------------------------------------------------------- 1 | # stop 2 | The stop command is used to stop a running server. 3 | ``` 4 | impulse stop 5 | ``` 6 | This is useful for stopping a server that is running in the background or to stop a server that is no longer needed. This also works for servers with an `inactiveTimeout` of 0 that would otherwise never stop. 7 | 8 | > [!NOTE] 9 | > Although the implementation is broker specific, the semantic of this command is to *hault* a running server without removing of freeing its underlying resources. This in theory allows for a faster resume of the server. For example with the Docker broker, the container is stopped and put into the `exited` state. The container is not removed from the system. 10 | 11 | ### Specify a server 12 | The server argument is the name of the server you would like to stop. This is the name you defined in the `servers` section of the `config.yaml` file. 13 | 14 | ### Examples 15 | ``` 16 | impulse stop smp 17 | ``` 18 | 19 | ### Permission Scope 20 | Use the `impulse.server.stop` permission scope to control who can use this command. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: dabb1e 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Transfer to server '...' 17 | 3. etc 18 | 19 | **Expected behavior** 20 | A clear and concise description of what you expected to happen. 21 | 22 | **Your config.yaml** 23 | ```yaml 24 | Attach or include your configuration 25 | MAKE SURE TO REMOVE ANY SENSITIVE VALUES 26 | ``` 27 | 28 | **Log Messages** 29 | If applicable, attach any log messages from Impulse. 30 | 31 | **Environment (please complete the following information):** 32 | - OS: [e.g. Windows/Linux (Distro)/MacOS] 33 | - What containers are you using 34 | - Any other relevant info about how Velocity is set up 35 | 36 | **Versions:** 37 | Which version of Impulse are you using? 38 | Which version of Velocity are you using? 39 | 40 | **Additional context** 41 | Add any other context about the problem here. 42 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - '**' 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | permissions: 13 | contents: read 14 | 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v2 18 | 19 | - name: Set up JDK 20 | uses: actions/setup-java@v2 21 | with: 22 | java-version: '17' 23 | distribution: 'adopt' 24 | 25 | - name: Build and test with Gradle 26 | run: ./gradlew build test jacocoTestReport 27 | 28 | - name: Upload JaCoCo reports to Codecov 29 | uses: codecov/codecov-action@v5 30 | with: 31 | file: '**/build/reports/jacoco/test/jacocoTestReport.xml' 32 | token: ${{ secrets.CODECOV_TOKEN }} 33 | slug: Arson-Club/Impulse 34 | flags: unittests 35 | 36 | - name: Upload Test Reports 37 | uses: codecov/test-results-action@v1 38 | with: 39 | files: '**/build/test-results/test/*.xml' 40 | token: ${{ secrets.CODECOV_TOKEN }} 41 | slug: Arson-Club/Impulse 42 | flags: unittests -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We are happy to accept contributions from people with all levels of experience. If you have a feature you would 4 | like to see, or a bug you would like fix, feel free tp open an PR. If you are new to the project take a look at our 5 | issues tagged 6 | with [good first issue](https://github.com/Arson-Club/Impulse/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22). 7 | 8 | Beyond that there are a few guidelines we would like you to follow: 9 | 10 | - If it is a particularly large feature, please open an issue first so we can discuss the implementation. 11 | - Make sure you cover your contributions with tests as much as possible 12 | - Once approved to run, make sure your code passes all tests and linters 13 | 14 | By contributing to this project, you agree that your contributions will be licensed under the project's license ( 15 | AGPLv3). We do not require a CLA, so you retain the copyright to your contributions. 16 | 17 | > [!TIP] 18 | > If you are looking to create a broker consider take a look at 19 | > our [Creating a Broker](https://arson-club.github.io/Impulse/contributing/creating-a-broker.html) guide. 20 | -------------------------------------------------------------------------------- /docs/reference/reconciliation.md: -------------------------------------------------------------------------------- 1 | # Reconciliation 2 | 3 | In Impulse, reconciliation is the process of ensuring that a servers configuration matches its running state. If they do 4 | not then action is taken to bring them back into alignment. The exact behavior of reconciliation is broker specific, but 5 | in general it will involve stopping the server, applying the new configuration, and restarting automatically. You can 6 | control this behavior by setting the `reconciliationBehavior` and `reconciliationGracePeriod` keys in the server 7 | configuration. 8 | 9 | ## Reconciliation Behavior 10 | 11 | ### `FORCE` 12 | 13 | When the reconciliation behavior is set to `FORCE` Impulse will immediately trigger a server restart to reconcile. The 14 | players are given a configurable grace period to finish up before the server is stopped. Duting this time a warning 15 | banner will be displayed to the players. 16 | 17 | ### `ON_STOP` 18 | 19 | When the reconciliation behavior is set to `ON_STOP` Impulse will only reconcile the server when it naturally restarts, 20 | 21 | ## Reconciliation Grace Period 22 | 23 | This is the amount of time in seconds that Impulse will wait before stopping a server during a reconciliation event. -------------------------------------------------------------------------------- /app/src/main/kotlin/club/arson/impulse/inject/modules/ServerModule.kt: -------------------------------------------------------------------------------- 1 | package club.arson.impulse.inject.modules 2 | 3 | import club.arson.impulse.api.config.ServerConfig 4 | import club.arson.impulse.api.server.Broker 5 | import club.arson.impulse.inject.providers.RegisteredServerProvider 6 | import com.google.inject.AbstractModule 7 | import com.google.inject.Provides 8 | import com.google.inject.Singleton 9 | import com.velocitypowered.api.proxy.ProxyServer 10 | import com.velocitypowered.api.proxy.server.RegisteredServer 11 | import org.slf4j.Logger 12 | 13 | class ServerModule( 14 | private val proxy: ProxyServer, 15 | private val config: ServerConfig, 16 | private val broker: Broker, 17 | private val logger: Logger? = null 18 | ) : AbstractModule() { 19 | override fun configure() { 20 | bind(object : com.google.inject.TypeLiteral() {}) 21 | .toProvider(RegisteredServerProvider(proxy, config, broker, logger)) 22 | } 23 | 24 | @Provides 25 | @Singleton 26 | fun provideBroker(): Broker { 27 | return broker 28 | } 29 | 30 | @Provides 31 | @Singleton 32 | fun provideServerConfig(): ServerConfig { 33 | return config 34 | } 35 | } -------------------------------------------------------------------------------- /app/src/test/kotlin/club/arson/impulse/inject/MockClassesProvider.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Impulse Server Manager for Velocity 3 | * Copyright (c) 2025 Dabb1e 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Affero General Public License as 7 | * published by the Free Software Foundation, either version 3 of the 8 | * License, or (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Affero General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Affero General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package club.arson.impulse.inject 20 | 21 | import com.google.inject.Provider 22 | import kotlin.reflect.KClass 23 | 24 | class MockClassesProvider : Provider>> { 25 | class TestBrokerConfig {} 26 | 27 | override fun get(): Map> { 28 | return mapOf("test" to TestBrokerConfig::class) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /api/src/main/kotlin/club/arson/impulse/api/server/Status.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Impulse Server Manager for Velocity 3 | * Copyright (c) 2025 Dabb1e 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Affero General Public License as 7 | * published by the Free Software Foundation, either version 3 of the 8 | * License, or (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Affero General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Affero General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package club.arson.impulse.api.server 20 | 21 | /** 22 | * Represents the status of a server 23 | * 24 | * @property RUNNING The server is running 25 | * @property STOPPED The server is stopped 26 | * @property REMOVED The server is removed 27 | * @property UNKNOWN The server status is unknown 28 | */ 29 | enum class Status { 30 | RUNNING, 31 | STOPPED, 32 | REMOVED, 33 | UNKNOWN 34 | } -------------------------------------------------------------------------------- /command-broker/build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Impulse Server Manager for Velocity 3 | * Copyright (c) 2025 Dabb1e 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Affero General Public License as 7 | * published by the Free Software Foundation, either version 3 of the 8 | * License, or (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Affero General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Affero General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | plugins { 20 | conventions.`impulse-base` 21 | conventions.`impulse-publish` 22 | conventions.jar 23 | } 24 | 25 | group = "club.arson.impulse" 26 | 27 | dependencies { 28 | implementation(project(":api")) 29 | } 30 | 31 | tasks.withType().configureEach { 32 | description = "Raw command and JAR based brokers for Impulse." 33 | } 34 | 35 | impulsePublish { 36 | artifact = tasks.named("jar").get() 37 | description = "Raw command and JAR based brokers for Impulse." 38 | } -------------------------------------------------------------------------------- /.github/workflows/deploy-docs.yml: -------------------------------------------------------------------------------- 1 | name: DeployMDBook 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | jobs: 8 | deploy: 9 | runs-on: ubuntu-latest 10 | environment: github-pages 11 | permissions: 12 | contents: write 13 | pages: write 14 | id-token: write 15 | steps: 16 | - uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | lfs: true 20 | - uses: actions-rust-lang/setup-rust-toolchain@v1 21 | - name: install latest mdbook 22 | run: | 23 | cargo install mdbook 24 | cargo install mdbook-alerts 25 | - name: Build Book 26 | run: | 27 | mdbook build 28 | - name: Set up JDK 29 | uses: actions/setup-java@v2 30 | with: 31 | java-version: '17' 32 | distribution: 'adopt' 33 | - name: Generate KDocs 34 | run: ./gradlew dokkaGenerate 35 | - name: Setup Pages 36 | uses: actions/configure-pages@v4 37 | - name: Combine KDocs and MDBook 38 | run: | 39 | cp -r build/dokka/html book/kdocs 40 | - name: Upload artifacts 41 | uses: actions/upload-pages-artifact@v3 42 | with: 43 | path: 'book' 44 | - name: Deploy to GitHub Pages 45 | id: deployment 46 | uses: actions/deploy-pages@v4 47 | -------------------------------------------------------------------------------- /api/build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Impulse Server Manager for Velocity 3 | * Copyright (c) 2025 Dabb1e 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Affero General Public License as 7 | * published by the Free Software Foundation, either version 3 of the 8 | * License, or (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Affero General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Affero General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | plugins { 20 | conventions.`impulse-base` 21 | conventions.`impulse-publish` 22 | conventions.jar 23 | 24 | `java-library` 25 | } 26 | 27 | group = "club.arson.impulse" 28 | 29 | java { 30 | withSourcesJar() 31 | } 32 | 33 | tasks.withType().configureEach { 34 | description = "API library for extending Impulse with your own plugins." 35 | } 36 | 37 | impulsePublish { 38 | artifact = tasks.named("jar").get() 39 | description = "API library for extending Impulse with your own plugins." 40 | } 41 | 42 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gradle" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | commit-message: 8 | prefix: "fix" 9 | open-pull-requests-limit: 5 10 | ignore: 11 | - dependency-name: "com.velocitypowered:velocity-api" # Ignore velocity so we can handle API updates 12 | update-types: ["version-update:semver-major"] 13 | - package-ecosystem: "gradle" 14 | directory: "/api/" 15 | schedule: 16 | interval: "weekly" 17 | commit-message: 18 | prefix: "fix" 19 | open-pull-requests-limit: 5 20 | ignore: 21 | - dependency-name: "com.velocitypowered:velocity-api" # Ignore velocity so we can handle API updates 22 | update-types: ["version-update:semver-major"] 23 | - package-ecosystem: "gradle" 24 | directory: "/app/" 25 | schedule: 26 | interval: "weekly" 27 | commit-message: 28 | prefix: "fix" 29 | open-pull-requests-limit: 5 30 | - package-ecosystem: "gradle" 31 | directory: "/docker-broker/" 32 | schedule: 33 | interval: "weekly" 34 | commit-message: 35 | prefix: "fix" 36 | open-pull-requests-limit: 5 37 | ignore: 38 | - dependency-name: "com.velocitypowered:velocity-api" # Ignore velocity so we can handle API updates 39 | update-types: ["version-update:semver-major"] 40 | -------------------------------------------------------------------------------- /docs/reference/command-broker.md: -------------------------------------------------------------------------------- 1 | # Command Broker 2 | 3 | The command broker allows you to execute arbitrary commands on the host machine in order to start a server. Impulse will 4 | then manage the associated process, assuming it is a minecraft server. This should be used with caution, or to integrate 5 | new server types for testing. For production you should use or [create](../contributing/creating-a-broker.md) a broker. 6 | 7 | > [!WARNING] 8 | > This broker will execute commands with the same permissions and user as the Velocity proxy. You should ensure to limit 9 | > the scope of this user as much as possible. **Do not run your proxy as root!** 10 | 11 | ## Configuration 12 | 13 | Command broker specific configuration values. These should be nested under the `cmd` key in the server configuration. 14 | 15 | | Key | Type | Description | Default | 16 | |--------------------|----------------|----------------------------------------------------------------------|---------| 17 | | `workingDirectory` | `string` | Working directory to set for the spawed subprocess | `none` | 18 | | `command` | `list[string]` | List of command and flags to execute to start a server | `none` | 19 | | `address` | `string` | Optional address to use for the server if using dynamic registration | | -------------------------------------------------------------------------------- /docs/reference/remove-command.md: -------------------------------------------------------------------------------- 1 | # remove 2 | The remove command is used to stop a server and free its underlying resources. 3 | ``` 4 | impulse remove 5 | ``` 6 | This is useful for making sure that the servers resources are completely freed up and that the server is no longer running. This command also works for servers with an `inactiveTimeout` of 0 that would otherwise never stop. The tradeoff is slower startup times as compared with the `stop` command. 7 | 8 | > [!NOTE] 9 | > The semantic of this command is to completely remove any underlying resources associated with the server. This effectively cleans up and frees any CPU or memory that was reserved for the server. Remove will neither delete the server from the `config.yaml` **nor remove any volume mounts or other persistent data**. 10 | 11 | > [!WARNING] 12 | > If using the Docker broker without a volume mount for the data directory (normally `/data`), all data will be lost when the server is removed. Be sure to use a volume mount to persist data across server removals if you want to keep your world. 13 | 14 | ### Specify a server 15 | The server argument is the name of the server you would like to remove. This is the name you defined in the `servers` section of the `config.yaml` file. 16 | 17 | ### Examples 18 | ``` 19 | impulse remove smp 20 | ``` 21 | 22 | ### Permission Scope 23 | Use the `impulse.server.remove` permission scope to control who can use this command. 24 | -------------------------------------------------------------------------------- /app/src/test/kotlin/club/arson/impulse/inject/MockFactoriesProvider.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Impulse Server Manager for Velocity 3 | * Copyright (c) 2025 Dabb1e 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Affero General Public License as 7 | * published by the Free Software Foundation, either version 3 of the 8 | * License, or (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Affero General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Affero General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package club.arson.impulse.inject 20 | 21 | import club.arson.impulse.api.server.BrokerFactory 22 | import com.google.inject.Provider 23 | import io.mockk.every 24 | import io.mockk.mockk 25 | 26 | class MockFactoriesProvider : Provider> { 27 | override fun get(): Set { 28 | val mockFactory = mockk() 29 | every { mockFactory.provides } returns listOf("test") 30 | every { mockFactory.createFromConfig(any(), any()) } returns Result.success(mockk(relaxed = true)) 31 | return setOf(mockFactory) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /command-broker/src/main/kotlin/club/arson/impulse/commandbroker/CommandBrokerConfig.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Impulse Server Manager for Velocity 3 | * Copyright (c) 2025 Dabb1e 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Affero General Public License as 7 | * published by the Free Software Foundation, either version 3 of the 8 | * License, or (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Affero General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Affero General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package club.arson.impulse.commandbroker 20 | 21 | import club.arson.impulse.api.config.BrokerConfig 22 | import kotlinx.serialization.Serializable 23 | 24 | /** 25 | * Configuration for the CommandBroker 26 | * 27 | * @property workingDirectory The working directory to run the command in 28 | * @property command The command to run 29 | * @property address The address to bind to (if using dynamic registration) 30 | */ 31 | @BrokerConfig("cmd") 32 | @Serializable 33 | data class CommandBrokerConfig( 34 | var workingDirectory: String, 35 | var command: List, 36 | var address: String? = null, 37 | ) -------------------------------------------------------------------------------- /docs/reference/advanced-installation.md: -------------------------------------------------------------------------------- 1 | # Advanced Installation 2 | 3 | > [!TIP] 4 | > For a basic installation and setup guide, see the [Getting Started](../getting_started/index.md) page. 5 | 6 | The default distribution of Impulse includes several default brokers. This is useful for getting started quickly, but 7 | not optimal for production environments. This section will outline how to use the "lite" distribution of Impulse to set 8 | up your server with just the brokers you need. Some benefits of this approach include: 9 | 10 | - Smaller jar size: The default distribution includes several brokers, which can be quite large. The "lite" distribution 11 | includes only the brokers you need. 12 | - Faster startup time: No need to initialize brokers you don't need. 13 | - Smaller attack surface: Fewer brokers means fewer potential vulnerabilities. 14 | 15 | ## Downloading the "Lite" Distribution 16 | 17 | You can find the "lite" distribution of Impulse on the [releases page](https://github.com/Arson-Club/Impulse/releases) 18 | or under the "Lite" channel on [Hangar](https://hangar.papermc.io/ArsonClub/Impulse). Download the jar file and place it 19 | in the plugins folder for Velocity. 20 | 21 | ## Adding a Broker 22 | 23 | To add a broker to your server, you will need to download the broker jar file and place it in the `plugins/impulse` 24 | folder in Velocity. The broker will be loaded automatically when the server starts. You can find first party brokers on 25 | the [releases page](https://github.com/Arson-Club/Impulse/releases). -------------------------------------------------------------------------------- /app/src/test/kotlin/club/arson/impulse/inject/TestModule.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Impulse Server Manager for Velocity 3 | * Copyright (c) 2025 Dabb1e 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Affero General Public License as 7 | * published by the Free Software Foundation, either version 3 of the 8 | * License, or (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Affero General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Affero General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package club.arson.impulse.inject 20 | 21 | import club.arson.impulse.api.server.BrokerFactory 22 | import com.google.inject.AbstractModule 23 | import com.google.inject.Scopes 24 | import com.google.inject.TypeLiteral 25 | import kotlin.reflect.KClass 26 | 27 | class TestModule() : AbstractModule() { 28 | override fun configure() { 29 | bind(object : TypeLiteral>() {}) 30 | .toProvider(MockFactoriesProvider::class.java) 31 | .`in`(Scopes.SINGLETON) 32 | bind(object : TypeLiteral>>() {}) 33 | .toProvider(MockClassesProvider::class.java) 34 | .`in`(Scopes.SINGLETON) 35 | } 36 | } -------------------------------------------------------------------------------- /docs/reference/jar-broker.md: -------------------------------------------------------------------------------- 1 | # JAR Broker 2 | 3 | The JAR broker is a specialization of the generic [command broker](command-broker.md). It is designed to give a better 4 | config interface when starting java based minecraft servers. As such its capabilities and limitations are the same as 5 | the command broker. 6 | 7 | ## Configuration 8 | 9 | JAR broker specific configuration values. These should be nested under the `jar` key in the server configuration. 10 | 11 | | Key | Type | Description | Default | 12 | |--------------------|----------------|-----------------------------------------------------------------------------------------------------------------|---------| 13 | | `workingDirectory` | `string` | Working directory to execute the jar in. This should probably be the root of your server data where the jar is. | `none` | 14 | | `jarFile` | `string` | Name of the jar file to run. | `none` | 15 | | `javaFlags` | `list[string]` | Flags to apply to the java JVM instance. | `[]` | 16 | | `flags` | `list[string]` | Flags to apply to the JAR | `[]` | 17 | | `address` | `string` | Optional address of the server if using dynamic registration | | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Impulse Server Manager for Velocity 3 | * Copyright (c) 2025 Dabb1e 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Affero General Public License as 7 | * published by the Free Software Foundation, either version 3 of the 8 | * License, or (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Affero General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Affero General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | @file:Suppress("UnstableApiUsage") 20 | 21 | rootProject.name = "impulse" 22 | 23 | pluginManagement { 24 | repositories { 25 | mavenCentral() 26 | gradlePluginPortal() 27 | } 28 | } 29 | 30 | dependencyResolutionManagement { 31 | repositories { 32 | mavenCentral() 33 | maven("https://repo.papermc.io/repository/maven-public/") { 34 | name = "papermc-repo" 35 | } 36 | maven("https://oss.sonatype.org/content/groups/public/") { 37 | name = "sonatype" 38 | } 39 | } 40 | } 41 | 42 | sequenceOf( 43 | "api", 44 | "app", 45 | "docker-broker", 46 | "command-broker", 47 | ).forEach { 48 | val p = ":$it" 49 | include(p) 50 | project(p).projectDir = file(it) 51 | } -------------------------------------------------------------------------------- /api/src/main/kotlin/club/arson/impulse/api/config/Configuration.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Impulse Server Manager for Velocity 3 | * Copyright (c) 2025 Dabb1e 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Affero General Public License as 7 | * published by the Free Software Foundation, either version 3 of the 8 | * License, or (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Affero General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Affero General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package club.arson.impulse.api.config 20 | 21 | import kotlinx.serialization.Serializable 22 | 23 | /** 24 | * Represents the configuration for Impulse 25 | * 26 | * @property instanceName A unique identifier for this instance of Impulse 27 | * @property servers A list of server configurations. See [ServerConfig] 28 | * @property serverMaintenanceInterval The interval in seconds between server maintenance tasks 29 | * @property messages Messages to be displayed to players. See [Messages] 30 | */ 31 | @Serializable 32 | data class Configuration( 33 | var instanceName: String = "velocity", 34 | var servers: List = listOf(), 35 | var serverMaintenanceInterval: Long = 300, 36 | var messages: Messages = Messages() 37 | ) -------------------------------------------------------------------------------- /docs/reference/brokers.md: -------------------------------------------------------------------------------- 1 | # Brokers 2 | 3 | Impulse offloads the low level server management to a "broker". These brokers deal with implementing the server 4 | abstractions from Impulse into a specific platform. This allows Impulse itself to be smaller. You only need to include 5 | the brokers you will be using. The docker broker is included in the default distribution. The "lite" distribution does 6 | not include any brokers. 7 | 8 | ## Dynamic Broker Loading 9 | 10 | Impulse is capable of dynamically loading additional brokers at startup. To add a broker, place the jar in 11 | Impulse's data directory (normally `plugins/impulse`). Hot reloading is not currently supported. You will need to 12 | restart Velocity to update add a new broker or update an existing one. 13 | 14 | ## First Party Brokers 15 | 16 | We provide several first party brokers. You can find more information on each broker below. 17 | > [!NOTE] 18 | > Not all brokers are available in the default distribution. You may need to add them to your Impulse installation. 19 | 20 | - [Docker](docker-broker.md) 21 | - [JAR](jar-broker.md) 22 | - [Kubernetes]() 23 | 24 | ## Third Party Brokers 25 | 26 | Additionally, you can create your own broker or source them from others. For more information see our guide 27 | on [creating a broker](../contributing/creating-a-broker.md). Here is a list of some notable third party brokers: 28 | > [!IMPORTANT] 29 | > These brokers are not tested or maintained by the Impulse team. Make sure to verify them and report any issues to the 30 | > respective authors. 31 | 32 | - [Crafty Controller](https://github.com/Thebestandgreatest/craftybroker) 33 | -------------------------------------------------------------------------------- /api/src/main/kotlin/club/arson/impulse/api/config/ShutdownBehavior.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Impulse Server Manager for Velocity 3 | * Copyright (c) 2025 Dabb1e 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Affero General Public License as 7 | * published by the Free Software Foundation, either version 3 of the 8 | * License, or (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Affero General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Affero General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package club.arson.impulse.api.config 20 | 21 | import kotlinx.serialization.Serializable 22 | 23 | /** 24 | * The behavior to take when a server receives a shutdown signal 25 | * 26 | */ 27 | @Serializable 28 | enum class ShutdownBehavior { 29 | /** 30 | * Stop the server. 31 | * 32 | * Normally this means leaving the resources for the server intact in a "paused" state, 33 | * but the implementation may vary depending on the server broker 34 | */ 35 | STOP, 36 | 37 | /** 38 | * Remove the server. 39 | * 40 | * This will delete the server resources and free up the resources (excluding persistent 41 | * volumes), but the implementation may vary depending on the server broker 42 | */ 43 | REMOVE 44 | } 45 | -------------------------------------------------------------------------------- /docs/getting_started/velocity_configuration.md: -------------------------------------------------------------------------------- 1 | # Velocity Configuration 2 | 3 | Velocity's configuration is stored in the `velocity.toml` file. For more information on all the available options, see 4 | [Velocity's configuration documentation](https://docs.papermc.io/velocity/configuration). 5 | 6 | For this guide we will only need to touch a few options. 7 | 8 | ## Servers 9 | 10 | Due to some limitations in the Velocity API, you need to define any server you would like to directly connect to through 11 | either the `try` or `forced-hoses` blocks in the `velocity.toml` file. We are going to add the connection information 12 | for our SMP server since we want all players to connect to it by default. We will later configure Impulse to adopt the 13 | server reference that velocity creates here. 14 | 15 | ```toml 16 | [servers] 17 | smp = "127.0.0.1:25566" 18 | ``` 19 | 20 | > [!IMPORTANT] 21 | > Make a note of the server "name" used here. We will need it later. 22 | 23 | ## Try 24 | 25 | The easiest way to get all our players to connect to our SMP by default is to set it as the first (and only) option in 26 | the `try` block. Impulse is not affected by this or the `forced-hosts` settings. 27 | 28 | ```toml 29 | try = ["smp"] 30 | ``` 31 | 32 | ## Player Info Forwarding 33 | 34 | Since we are running Velocity in online mode, we can set up player info forwarding to our SMP server so that people get 35 | their skins and correct usernames. Simply change the following option: 36 | 37 | ```toml 38 | player-info-forwarding = "modern" 39 | ``` 40 | 41 | That is it for the Velocity configuration! Save the file and either reload it with `/velocity reload` or restart the 42 | proxy. -------------------------------------------------------------------------------- /api/src/main/kotlin/club/arson/impulse/api/server/BrokerFactory.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Impulse Server Manager for Velocity 3 | * Copyright (c) 2025 Dabb1e 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Affero General Public License as 7 | * published by the Free Software Foundation, either version 3 of the 8 | * License, or (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Affero General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Affero General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package club.arson.impulse.api.server 20 | 21 | import club.arson.impulse.api.config.ServerConfig 22 | import org.slf4j.Logger 23 | 24 | /** 25 | * API used to implemnt a broker factory. This is used to create brokers from a configuration. 26 | */ 27 | interface BrokerFactory { 28 | /** 29 | * A list of the broker types this factory can create 30 | */ 31 | val provides: List 32 | 33 | /** 34 | * Create a broker from a configuration 35 | * 36 | * @param config The [ServerConfig] used to create the broker from 37 | * @param logger An optional logger to use for messages in the broker 38 | * @return A [Result] containing the created broker or an error 39 | */ 40 | fun createFromConfig(config: ServerConfig, logger: Logger? = null): Result 41 | } -------------------------------------------------------------------------------- /api/src/main/kotlin/club/arson/impulse/api/config/ServerConfig.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Impulse Server Manager for Velocity 3 | * Copyright (c) 2025 Dabb1e 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Affero General Public License as 7 | * published by the Free Software Foundation, either version 3 of the 8 | * License, or (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Affero General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Affero General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package club.arson.impulse.api.config 20 | 21 | import kotlinx.serialization.Serializable 22 | import kotlinx.serialization.Transient 23 | 24 | /** 25 | * Represents a server configuration 26 | * 27 | * @property name The name of the server. Must match the server name in Velocity's configuration 28 | * @property type The type of server. Must be either "docker" or "kubernetes" 29 | * @property lifecycleSettings The lifecycle settings for the server 30 | * @property config The broker specific configuration, this is not set directly, but rather injected by the config manager 31 | */ 32 | @Serializable 33 | data class ServerConfig( 34 | var name: String, 35 | var type: String, 36 | var lifecycleSettings: LifecycleSettings = LifecycleSettings(), 37 | @Transient var config: Any? = null 38 | ) -------------------------------------------------------------------------------- /docs/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | [Introduction](README.md) 4 | 5 | # Getting Started 6 | 7 | - [Overview](./getting_started/index.md) 8 | - [Prerequisites](./getting_started/prerequisites.md) 9 | - [Installation](./getting_started/installation.md) 10 | - [Velocity Configuration](./getting_started/velocity_configuration.md) 11 | - [Impulse Configuration](./getting_started/impulse_configuration.md) 12 | - [Docker](./getting_started/docker_broker.md) 13 | - [JAR](./getting_started/jar_broker.md) 14 | - [Kubernetes]() 15 | - [Connecting and Exploring](./getting_started/connecting_and_exploring.md) 16 | 17 | # Reference 18 | 19 | - [Configuration](reference/configuration.md) 20 | - [Commands](reference/commands.md) 21 | - [start](reference/start-command.md) 22 | - [stop](reference/stop-command.md) 23 | - [remove](reference/remove-command.md) 24 | - [reconcile](reference/reconcile-command.md) 25 | - [status](reference/status-command.md) 26 | - [pin](reference/pin-command.md) 27 | - [unpin](reference/unpin-command.md) 28 | - [Brokers](reference/brokers.md) 29 | - [Docker](reference/docker-broker.md) 30 | - [Command](reference/command-broker.md) 31 | - [JAR](reference/jar-broker.md) 32 | - [Kubernetes]() 33 | - [Reconciliation](reference/reconciliation.md) 34 | - [Custom Events](reference/custom-events.md) 35 | - [Advanced Installation](reference/advanced-installation.md) 36 | - [Examples]() 37 | - [Persistent Lobby]() 38 | - [Minigames Server]() 39 | - [Migrating Existing Server]() 40 | 41 | # Other 42 | 43 | - [KDocs](./kdocs.md) 44 | - [Contributing](contributing/CONTRIBUTING.md) 45 | - [Creating a Broker](contributing/creating-a-broker.md) 46 | - [Licensing](./licensing.md) 47 | -------------------------------------------------------------------------------- /app/src/main/kotlin/club/arson/impulse/commands/ImpulseCommand.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Impulse Server Manager for Velocity 3 | * Copyright (c) 2025 Dabb1e 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Affero General Public License as 7 | * published by the Free Software Foundation, either version 3 of the 8 | * License, or (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Affero General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Affero General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package club.arson.impulse.commands 20 | 21 | import com.velocitypowered.api.command.BrigadierCommand 22 | 23 | fun createImpulseCommand(): BrigadierCommand { 24 | val startNode = createStartServerCommand() 25 | val commandNode = BrigadierCommand.literalArgumentBuilder("impulse") 26 | .then(startNode) 27 | .then( 28 | BrigadierCommand.literalArgumentBuilder("warm") 29 | .requires { source -> source.hasPermission("impulse.server.start") } 30 | .redirect(startNode.build())) 31 | .then(createStopServerCommand()) 32 | .then(createRemoveServerCommand()) 33 | .then(createReconcileCommand()) 34 | .then(createServerStatusCommand()) 35 | .then(createPinServerCommand()) 36 | .then(createUnpinServerCommand()) 37 | return BrigadierCommand(commandNode) 38 | } -------------------------------------------------------------------------------- /docker-broker/build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Impulse Server Manager for Velocity 3 | * Copyright (c) 2025 Dabb1e 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Affero General Public License as 7 | * published by the Free Software Foundation, either version 3 of the 8 | * License, or (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Affero General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Affero General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar 20 | 21 | plugins { 22 | conventions.`impulse-base` 23 | conventions.`impulse-publish` 24 | conventions.`shadow-jar` 25 | } 26 | group = "club.arson.impulse" 27 | 28 | dependencies { 29 | implementation(libs.bundles.docker) 30 | implementation(libs.kotlinxCoroutines) 31 | implementation(project(":api")) 32 | } 33 | 34 | tasks.withType().configureEach { 35 | description = "Docker broker for Impulse." 36 | relocate("com.github.docker-java", "club.arson.impulse.docker-java") 37 | relocate("org.jetbrains.kotlinx", "club.arson.impulse.kotlinx") 38 | } 39 | 40 | impulsePublish { 41 | artifact = tasks.named("shadowJar").get() 42 | description = "Docker broker for Impulse." 43 | licenses = listOf( 44 | impulseLicense, 45 | kamlLicense, 46 | dockerLicense 47 | ) 48 | } -------------------------------------------------------------------------------- /api/src/main/kotlin/club/arson/impulse/api/events/RegisterBrokerEvent.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Impulse Server Manager for Velocity 3 | * Copyright (c) 2025 Dabb1e 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Affero General Public License as 7 | * published by the Free Software Foundation, either version 3 of the 8 | * License, or (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Affero General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Affero General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package club.arson.impulse.api.events 20 | 21 | import com.velocitypowered.api.event.ResultedEvent 22 | import com.velocitypowered.api.event.ResultedEvent.GenericResult 23 | import java.nio.file.Path 24 | 25 | /** 26 | * An event to fire to dynamically register your broker 27 | * 28 | * If a broker is being provided as a full Velocity plugin, this event should be fired to register the jar with Impulse 29 | * @property jarPath the path to the jar file 30 | * @property result the result of the event 31 | * @constructor create a new broker registration event 32 | */ 33 | class RegisterBrokerEvent( 34 | var jarPath: Path, 35 | @JvmField var result: GenericResult = GenericResult.allowed() 36 | ) : ResultedEvent { 37 | override fun getResult(): GenericResult { 38 | return result 39 | } 40 | 41 | override fun setResult(result: GenericResult) { 42 | this.result = result 43 | } 44 | } -------------------------------------------------------------------------------- /docs/licensing.md: -------------------------------------------------------------------------------- 1 | # Licensing 2 | 3 | Impulse is licensed under AGPL-3.0. You can find the full license text in 4 | the [LICENSE](https://github.com/Arson-Club/Impulse/LICENSE.txt) file. 5 | 6 | ## External Libraries 7 | 8 | Impulse distributes the following libraries. You can find more information about their licensing on their respective 9 | pages. 10 | 11 | ### Impulse App 12 | 13 | | Library | License | Link | 14 | |--------------|------------|------------------------------------------------------------------| 15 | | ClassGraph | MIT | [Github](https://github.com/classgraph/classgraph) | 16 | | Kaml | Apache-2.0 | [Github](https://github.com/charleskorn/kaml) | 17 | | Velocity-API | GPL-3.0 | [Github](https://github.com/PaperMC/Velocity?tab=readme-ov-file) | 18 | 19 | ### Docker Broker 20 | 21 | | Library | License | Link | 22 | |--------------------|------------|--------------------------------------------------------| 23 | | Docker Java Client | Apache-2.0 | [Github](https://github.com/docker-java/docker-java) | 24 | | Kotlinx Coroutines | Apache-2.0 | [Github](https://github.com/Kotlin/kotlinx.coroutines) | 25 | 26 | ## Documentation 27 | 28 | This documentation also uses the following technologies and libraries: 29 | 30 | | Library | License | Link | 31 | |-----------------------|---------|-----------------------------------------------------------| 32 | | Catppuccin for mdBook | MIT | [Github](https://github.com/catppuccin/mdBook) | 33 | | mdbook-alert | MIT | [Github](https://github.com/lambdalisue/rs-mdbook-alerts) | 34 | | mdbook | MLP-2.0 | [Github](https://github.com/rust-lang/mdBook) | 35 | 36 | -------------------------------------------------------------------------------- /docs/getting_started/impulse_configuration.md: -------------------------------------------------------------------------------- 1 | # Impulse Configuration 2 | 3 | Impulse maintains its own configuration file named `config.yaml`. It can be found in the `plugins/impulse` directory. 4 | If it does not exist you can create it. For our simple SMP we can keep most of the defaults. For now lets set our 5 | `instanceName`. 6 | 7 | `plugins/impulse/config.yaml`: 8 | 9 | ```yaml 10 | instanceName: MyCoolSMP 11 | ``` 12 | 13 | The instance name is used internally by Impulse to identify your Velocity server. This is necessary for some brokers 14 | that may be managing servers for multiple Velocity instances. It should be unique per Velocity server. 15 | 16 | ## Selecting a Broker 17 | 18 | In Velocity a "broker" is an interface that Impulse uses to actually interact with different platforms that can host 19 | minecraft servers. For the next step select which broker you would like to use, and follow the instructions for that 20 | broker. You can find a basic description of each broker below with pros and cons. For more in depth information see 21 | the [brokers](../reference/brokers.md) documentation. 22 | 23 | ### Docker 24 | 25 | The Docker broker is a good choice for most users. It offers a good balance of flexibility and ease of setup. It also 26 | allows for running servers on multiple machines. 27 | 28 | [Continue to Docker Broker Setup](docker_broker.md) 29 | 30 | ### JAR 31 | 32 | The JAR broker is the simplest broker. It primarily designed for a single server setup and development. It only supports 33 | running servers on the same machine as Velocity. 34 | 35 | [Continue to JAR Broker Setup](jar_broker.md) 36 | 37 | ### Kubernetes 38 | 39 | > [!WARNING] 40 | > This broker in not yet available. It is planned for a future release. 41 | 42 | The Kubernetes broker is for advanced setups and those already familiar with Kubernetes. It is the most powerful broker, 43 | but also significantly more complex to setup. 44 | 45 | [NOT YET AVAILABLE]() 46 | -------------------------------------------------------------------------------- /docs/getting_started/prerequisites.md: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | 3 | ## Velocity 4 | 5 | > [!WARNING] 6 | > Impulse does not condone and will not provide support for proxies running in offline mode. 7 | 8 | This guide assumes you already have a working velocity server. If you are setting up from scratch, you can follow 9 | [Velocity's getting started guide](https://docs.papermc.io/velocity/getting-started). Once configured, you can proceed 10 | with setting up Impulse. Additionally, this guide will assume you 11 | have [configured player forwarding](https://docs.papermc.io/velocity/player-information-forwarding) and are running your 12 | proxy in online mode. 13 | 14 | ## Brokers 15 | 16 | Later in this guide we will be configuring our backed servers using one of Impulse's broker implementations. Depending 17 | on which broker you choose, you may need to install additional software on your backend servers. 18 | 19 | ### Docker 20 | 21 | Currently Docker is the only broker implementation available. Any computer you would like Impulse to run servers on via 22 | this 23 | broker must have docker installed. You can follow the 24 | [official docker installation guide](https://docs.docker.com/get-docker/). 25 | 26 | In this example, are running our backend servers on the same machine as our proxy. 27 | 28 | > [!TIP] 29 | > For more information on the docker broker itself, see 30 | > the [docker broker documentation](../reference/docker-broker.md). 31 | > You can also find more complex examples in our [Examples]() section. 32 | 33 | ### JAR 34 | 35 | The JAR broker simply requires the Java Runtime to be installed. Since it can only manage servers on the same machine as 36 | Velocity, this should already be installed. You will also need to download the server JAR file you plan to run. In this 37 | example, we will be using the [Fabric server](https://fabricmc.net/use/server/). 38 | 39 | > [!TIP] 40 | > For more information on the JAR broker itself, see 41 | > the [JAR broker documentation](../reference/jar-broker.md). -------------------------------------------------------------------------------- /app/src/main/kotlin/club/arson/impulse/commands/StopServer.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Impulse Server Manager for Velocity 3 | * Copyright (c) 2025 Dabb1e 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Affero General Public License as 7 | * published by the Free Software Foundation, either version 3 of the 8 | * License, or (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Affero General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Affero General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package club.arson.impulse.commands 20 | 21 | import club.arson.impulse.ServiceRegistry 22 | import com.mojang.brigadier.Command 23 | import com.mojang.brigadier.builder.LiteralArgumentBuilder 24 | import com.velocitypowered.api.command.CommandSource 25 | import net.kyori.adventure.text.Component 26 | 27 | fun createStopServerCommand(): LiteralArgumentBuilder { 28 | return createGenericServerCommand("stop") { context, serverName -> 29 | context.source.sendMessage( 30 | Component.text("Stopping $serverName") 31 | ) 32 | val stopResult = ServiceRegistry.instance.serverManager?.getServer(serverName)?.stopServer() 33 | if (stopResult == null || stopResult.isFailure) { 34 | context.source.sendMessage( 35 | Component.text("Error: failed to stop server $serverName") 36 | ) 37 | } else { 38 | context.source.sendMessage( 39 | Component.text("Server $serverName stopped successfully") 40 | ) 41 | } 42 | return@createGenericServerCommand Command.SINGLE_SUCCESS 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/src/test/kotlin/club/arson/impulse/ServerBrokerTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Impulse Server Manager for Velocity 3 | * Copyright (c) 2025 Dabb1e 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Affero General Public License as 7 | * published by the Free Software Foundation, either version 3 of the 8 | * License, or (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Affero General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Affero General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package club.arson.impulse 20 | 21 | import club.arson.impulse.api.config.ServerConfig 22 | import club.arson.impulse.inject.TestModule 23 | import club.arson.impulse.server.ServerBroker 24 | import com.google.inject.Guice 25 | import io.mockk.every 26 | import io.mockk.mockk 27 | import org.junit.jupiter.api.BeforeEach 28 | import org.junit.jupiter.api.Test 29 | 30 | class ServerBrokerTest { 31 | private lateinit var serverBroker: ServerBroker 32 | 33 | @BeforeEach 34 | fun setUp() { 35 | val injector = Guice.createInjector(TestModule()) 36 | serverBroker = injector.getInstance(ServerBroker::class.java) 37 | } 38 | 39 | @Test 40 | fun testCreateFromConfig() { 41 | val serverConfig = mockk() 42 | every { serverConfig.type } returnsMany listOf("test", "invalid") 43 | 44 | // We should find something 45 | var result = serverBroker.createFromConfig(serverConfig) 46 | assert(result.isSuccess) 47 | 48 | // No broker should be found 49 | result = serverBroker.createFromConfig(serverConfig) 50 | assert(result.isFailure) 51 | } 52 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/club/arson/impulse/inject/modules/BaseModule.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Impulse Server Manager for Velocity 3 | * Copyright (c) 2025 Dabb1e 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Affero General Public License as 7 | * published by the Free Software Foundation, either version 3 of the 8 | * License, or (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Affero General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Affero General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package club.arson.impulse.inject.modules 20 | 21 | import club.arson.impulse.Impulse 22 | import com.google.inject.AbstractModule 23 | import com.google.inject.Provides 24 | import com.velocitypowered.api.proxy.ProxyServer 25 | import org.slf4j.Logger 26 | import java.nio.file.Path 27 | import javax.inject.Qualifier 28 | 29 | @Qualifier 30 | @Target(AnnotationTarget.FIELD, AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.FUNCTION) 31 | @Retention(AnnotationRetention.RUNTIME) 32 | annotation class PluginDir 33 | 34 | class BaseModule( 35 | val plugin: Impulse, 36 | val proxyServer: ProxyServer, 37 | val pluginDirectory: Path, 38 | val logger: Logger, 39 | ) : AbstractModule() { 40 | @Provides 41 | fun providePlugin(): Impulse { 42 | return plugin 43 | } 44 | 45 | @Provides 46 | fun provideProxyServer(): ProxyServer { 47 | return proxyServer 48 | } 49 | 50 | @Provides 51 | fun provideLogger(): Logger { 52 | return logger 53 | } 54 | 55 | @Provides 56 | @PluginDir 57 | fun providesPluginDirectory(): Path { 58 | return pluginDirectory 59 | } 60 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/club/arson/impulse/commands/StartServer.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Impulse Server Manager for Velocity 3 | * Copyright (c) 2025 Dabb1e 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Affero General Public License as 7 | * published by the Free Software Foundation, either version 3 of the 8 | * License, or (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Affero General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Affero General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package club.arson.impulse.commands 20 | 21 | import club.arson.impulse.ServiceRegistry 22 | import com.mojang.brigadier.Command 23 | import com.mojang.brigadier.builder.LiteralArgumentBuilder 24 | import com.velocitypowered.api.command.CommandSource 25 | import net.kyori.adventure.text.Component 26 | 27 | fun createStartServerCommand(): LiteralArgumentBuilder { 28 | return createGenericServerCommand("start") { context, serverName -> 29 | context.source.sendMessage( 30 | Component.text("Warming $serverName") 31 | ) 32 | val startResult = ServiceRegistry.instance.serverManager?.getServer(serverName)?.startServer() 33 | if (startResult == null || startResult.isFailure) { 34 | context.source.sendMessage( 35 | Component.text("Error: failed to start server $serverName") 36 | ) 37 | } else { 38 | startResult.getOrNull()?.awaitReady() 39 | context.source.sendMessage( 40 | Component.text("Server $serverName started successfully") 41 | ) 42 | } 43 | return@createGenericServerCommand Command.SINGLE_SUCCESS 44 | } 45 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/club/arson/impulse/inject/modules/BrokerModule.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Impulse Server Manager for Velocity 3 | * Copyright (c) 2025 Dabb1e 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Affero General Public License as 7 | * published by the Free Software Foundation, either version 3 of the 8 | * License, or (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Affero General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Affero General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package club.arson.impulse.inject.modules 20 | 21 | import club.arson.impulse.api.server.BrokerFactory 22 | import club.arson.impulse.inject.providers.BrokerConfigProvider 23 | import club.arson.impulse.inject.providers.BrokerFactoryProvider 24 | import com.google.inject.AbstractModule 25 | import com.google.inject.Scopes 26 | import org.slf4j.Logger 27 | import kotlin.reflect.KClass 28 | 29 | /** 30 | * Guice module for the broker system 31 | * 32 | * @property logger the logger to use for the broker system 33 | * @constructor creates a new broker module 34 | */ 35 | class BrokerModule(private val logger: Logger? = null) : AbstractModule() { 36 | /** 37 | * Binds the BrokerFactory and Broker Configuration providers for the injector 38 | */ 39 | override fun configure() { 40 | bind(object : com.google.inject.TypeLiteral>() {}) 41 | .toProvider(BrokerFactoryProvider(logger)) 42 | .`in`(Scopes.SINGLETON) 43 | bind(object : com.google.inject.TypeLiteral>>() {}) 44 | .toProvider(BrokerConfigProvider(logger)) 45 | .`in`(Scopes.SINGLETON) 46 | } 47 | } -------------------------------------------------------------------------------- /api/src/main/kotlin/club/arson/impulse/api/config/Messages.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Impulse Server Manager for Velocity 3 | * Copyright (c) 2025 Dabb1e 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Affero General Public License as 7 | * published by the Free Software Foundation, either version 3 of the 8 | * License, or (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Affero General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Affero General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package club.arson.impulse.api.config 20 | 21 | import kotlinx.serialization.Serializable 22 | 23 | /** 24 | * Message text for user facing messages 25 | * 26 | * These messages are used when presenting information to players. They support [MiniMessage](https://docs.advntr.dev/minimessage/format) 27 | * format. Customize these messages to match your server's branding. 28 | * 29 | * @property startupError The message to display when the server times out during startup 30 | * @property reconcileRestartTitle The title of the message to display during the grace period before server reconciliation (restarts) 31 | * @property reconcileRestartMessage The message to display during the grace period before server reconciliation (restarts) 32 | */ 33 | @Serializable 34 | data class Messages( 35 | var startupError: String = "Server is starting, please try again in a moment...\nIf this issue persists, please contact an administrator", 36 | var reconcileRestartTitle: String = "Server is Restarting...", 37 | var reconcileRestartMessage: String = "server restart imminent", 38 | var autoStartDisabled: String = "Autostart is disabled for this server" 39 | ) 40 | -------------------------------------------------------------------------------- /docs/getting_started/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | ## Download 4 | 5 | The Impulse jar file can be downloaded from serveral sources: 6 | 7 | - [Modrinth](https://modrinth.com/plugin/impulse-server-manager) 8 | - [Hangar](https://hangar.papermc.io/ArsonClub/Impulse) 9 | - [Offical Github Releases](https://github.com/Arson-Club/Impulse/releases) 10 | 11 | For people that need it in a package format, we also publish to 12 | the [Github Maven Repository](https://github.com/orgs/Arson-Club/packages?repo_name=Impulse). 13 | 14 | ## Installation 15 | 16 | To install Impulse simply place the jar file in your Velocity server's `plugins` folder and restart Velocity if it is 17 | running. You can verify Impulse is running by checking the console for the following messages: 18 | 19 | ``` 20 | [18:52:15 INFO] [Impulse]: Initializing Impulse 21 | [18:52:15 INFO] [Impulse]: Configuration reloaded 22 | ``` 23 | 24 | ## Alternative Versions 25 | 26 | > [!NOTE] 27 | > These versions are for people that are looking to try out new features before others. They are not recommended for 28 | > production servers and may be generally unstable. 29 | 30 | ### Pre-Release 31 | 32 | We will publish pre-release builds to our [Github Releases](https://github.com/Arson-Club/Impulse/releases) page 33 | and [Hangar Snapshots](https://hangar.papermc.io/ArsonClub/Impulse/versions?channel=Snapshot&platform=VELOCITY) channel. 34 | These are often release candidates that have not yet been fully tested, or that we are looking for feedback on. They are 35 | the most stable of our alternative versions since in theory any one could be promoted to a full release. 36 | 37 | ### Nightly 38 | 39 | We maintain a nightly build channel. As the name implies this is a nightly build of `main` bugs and all. You can find 40 | obtain these from our [Github Actions]() page. These are the least stable of our versions. 41 | 42 | ## Advanced Installation 43 | 44 | For more advanced installation options, such as custom brokers or using our "minimal" jar, see 45 | the [Advanced Installation](../reference/advanced-installation.md) guide. -------------------------------------------------------------------------------- /app/src/main/kotlin/club/arson/impulse/commands/RemoveServer.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Impulse Server Manager for Velocity 3 | * Copyright (c) 2025 Dabb1e 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Affero General Public License as 7 | * published by the Free Software Foundation, either version 3 of the 8 | * License, or (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Affero General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Affero General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package club.arson.impulse.commands 20 | 21 | import club.arson.impulse.ServiceRegistry 22 | import com.mojang.brigadier.Command 23 | import com.mojang.brigadier.builder.LiteralArgumentBuilder 24 | import com.velocitypowered.api.command.CommandSource 25 | import net.kyori.adventure.text.Component 26 | 27 | fun createRemoveServerCommand(): LiteralArgumentBuilder { 28 | return createGenericServerCommand("remove") { context, serverName -> 29 | context.source.sendMessage(Component.text("Removing server: $serverName")) 30 | 31 | val serverManager = ServiceRegistry.instance.serverManager 32 | val removeResult = serverManager?.getServer(serverName)?.removeServer() 33 | ?: Result.failure(Throwable("Server manager not available")) 34 | 35 | removeResult 36 | .onSuccess { 37 | context.source.sendMessage(Component.text("Server '$serverName' removed successfully")) 38 | } 39 | .onFailure { error -> 40 | context.source.sendMessage( 41 | Component.text("Error: Failed to remove server '$serverName': ${error}") 42 | ) 43 | } 44 | 45 | Command.SINGLE_SUCCESS 46 | } 47 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/club/arson/impulse/commands/ReconcileCommand.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Impulse Server Manager for Velocity 3 | * Copyright (c) 2025 Dabb1e 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Affero General Public License as 7 | * published by the Free Software Foundation, either version 3 of the 8 | * License, or (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Affero General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Affero General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package club.arson.impulse.commands 20 | 21 | import club.arson.impulse.ServiceRegistry 22 | import com.mojang.brigadier.Command 23 | import com.mojang.brigadier.builder.LiteralArgumentBuilder 24 | import com.velocitypowered.api.command.CommandSource 25 | import net.kyori.adventure.text.Component 26 | 27 | fun createReconcileCommand(): LiteralArgumentBuilder { 28 | return createGenericServerCommand("reconcile") { context, serverName -> 29 | context.source.sendMessage( 30 | Component.text("Reconciling $serverName") 31 | ) 32 | ServiceRegistry.instance.configManager?.servers?.find { it.name == serverName }?.let { server -> 33 | val res = ServiceRegistry.instance.serverManager?.getServer(serverName)?.reconcile(server) 34 | if (res?.isSuccess == true) { 35 | context.source.sendMessage( 36 | Component.text("Server $serverName reconciled successfully") 37 | ) 38 | } else { 39 | context.source.sendMessage( 40 | Component.text("Error: failed to reconcile server $serverName") 41 | ) 42 | } 43 | } 44 | return@createGenericServerCommand Command.SINGLE_SUCCESS 45 | } 46 | } -------------------------------------------------------------------------------- /docs/contributing/creating-a-broker.md: -------------------------------------------------------------------------------- 1 | # Creating a Broker 2 | 3 | Creating a custom broker for Impulse is a simple process! This guide will walk you through the steps to create a broker 4 | for your favorite server software. 5 | 6 | > [!TIP] 7 | > The [docker broker source](https://github.com/Arson-Club/Impulse/tree/main/docker-broker) is a great example to follow 8 | > when creating your own broker. 9 | 10 | ## Including the API 11 | 12 | The first step is to include the Impulse API in your project. We publish to the GitHub Package Registry, so you just 13 | have to add the following to your `build.gradle` file: 14 | 15 | ```groovy 16 | dependencies { 17 | implementation 'club.arson.impulse:impulse-api' 18 | } 19 | 20 | repositories { 21 | maven { 22 | name = "Impulse" 23 | url = uri("https://maven.pkg.github.com/Arson-Club/Impulse") 24 | } 25 | } 26 | ``` 27 | 28 | ## The Broker Interface 29 | 30 | The core component of a broker is its implementation of the `Broker` interface (`club.arson.impulse.api.server.Broker`). 31 | Here you will implement all the functions required for impulse to interact with the server software. For specifics on 32 | what each function does, refer to the [KDocs](https://arson-club.github.io/Impulse/kdocs/index.html). 33 | 34 | ## The Broker Configuration 35 | 36 | Most brokers will require some sort of configuration to be able to set up and manage servers properly. To do this, 37 | simply create a data class and annotate it with `@BrokerConfig("your-broker-id")`. Impulse will handle loading your 38 | configuration for you. Make sure to include a `@Serializable` annotation on the class as well. 39 | 40 | ## The Broker Factory 41 | 42 | The final step is to implement the `BrokerFactory` interface (`club.arson.impulse.api.server.BrokerFactory`). This is 43 | used by Impulse to create instances of your broker as needed. The most important thing here is the `NAME` field. It will 44 | be used to identify your broker in the configuration file, and to link it to your Configuration class. 45 | 46 | > [!WARNING] 47 | > You must provide at least a Factory and Configuration class for Impulse to load your broker. The IDs must match 48 | > ***exactly***. 49 | -------------------------------------------------------------------------------- /docker-broker/src/main/kotlin/club/arson/impulse/dockerbroker/DockerBrokerFactory.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Impulse Server Manager for Velocity 3 | * Copyright (c) 2025 Dabb1e 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Affero General Public License as 7 | * published by the Free Software Foundation, either version 3 of the 8 | * License, or (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Affero General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Affero General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package club.arson.impulse.dockerbroker 20 | 21 | import club.arson.impulse.api.config.ServerConfig 22 | import club.arson.impulse.api.server.Broker 23 | import club.arson.impulse.api.server.BrokerFactory 24 | import org.slf4j.Logger 25 | 26 | /** 27 | * Factory interface used to dynamically register and create docker brokers 28 | */ 29 | class DockerBrokerFactory : BrokerFactory { 30 | /** 31 | * List of broker types that this factory provides 32 | */ 33 | override val provides = listOf("docker") 34 | 35 | /** 36 | * Create a docker broker from a ServerConfig Object 37 | * 38 | * We check to make sure that the ServerConfig contains a DockerServerConfig before 39 | * creation. 40 | * @param config Server configuration to create a docker broker for 41 | * @param logger Logger ref for log messages 42 | * @return A result containing a docker broker if we were able to create one for the server, else an error 43 | */ 44 | override fun createFromConfig(config: ServerConfig, logger: Logger?): Result { 45 | return (config.config as? DockerServerConfig)?.let { conf -> 46 | Result.success(DockerBroker(config, logger)) 47 | } ?: Result.failure(IllegalArgumentException("Expected Docker specific config and got something else!")) 48 | } 49 | } -------------------------------------------------------------------------------- /command-broker/src/main/kotlin/club/arson/impulse/commandbroker/JarBrokerConfig.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Impulse Server Manager for Velocity 3 | * Copyright (c) 2025 Dabb1e 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Affero General Public License as 7 | * published by the Free Software Foundation, either version 3 of the 8 | * License, or (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Affero General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Affero General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package club.arson.impulse.commandbroker 20 | 21 | import club.arson.impulse.api.config.BrokerConfig 22 | import kotlinx.serialization.Serializable 23 | 24 | /** 25 | * Configuration for a jar broker 26 | * 27 | * @property workingDirectory The working directory to run the jar in 28 | * @property address The address to bind the server to if using dynamic registration 29 | * @property jarFile The jar file to run 30 | * @property javaFlags Flags to pass to the JVM 31 | * @property flags Flags to pass to the jar process 32 | */ 33 | @BrokerConfig("jar") 34 | @Serializable 35 | data class JarBrokerConfig( 36 | var workingDirectory: String, 37 | var address: String? = null, 38 | var jarFile: String, 39 | var javaFlags: List = emptyList(), 40 | var flags: List = emptyList() 41 | ) 42 | 43 | /** 44 | * Convert a JarBrokerConfig to a CommandBrokerConfig 45 | * 46 | * The JarBroker is a wrapper around the CommandBroker, so we need to convert the JarBrokerConfig to a CommandBrokerConfig 47 | * @param config The JarBrokerConfig to convert 48 | * @return The equivalent CommandBrokerConfig 49 | */ 50 | fun toCommandBrokerConfig(config: JarBrokerConfig): CommandBrokerConfig { 51 | return CommandBrokerConfig( 52 | config.workingDirectory, 53 | listOf("java") + config.javaFlags + listOf("-jar", config.jarFile) + config.flags, 54 | config.address 55 | ) 56 | } -------------------------------------------------------------------------------- /command-broker/src/main/kotlin/club/arson/impulse/commandbroker/CommandBrokerFactory.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Impulse Server Manager for Velocity 3 | * Copyright (c) 2025 Dabb1e 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Affero General Public License as 7 | * published by the Free Software Foundation, either version 3 of the 8 | * License, or (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Affero General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Affero General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package club.arson.impulse.commandbroker 20 | 21 | import club.arson.impulse.api.config.ServerConfig 22 | import club.arson.impulse.api.server.Broker 23 | import club.arson.impulse.api.server.BrokerFactory 24 | import org.slf4j.Logger 25 | 26 | /** 27 | * Factory interface used to dynamically register and create jar brokers 28 | */ 29 | class CommandBrokerFactory : BrokerFactory { 30 | /** 31 | * This broker is designed to run raw jar files on the server 32 | */ 33 | override val provides: List = listOf("jar", "cmd") 34 | 35 | /** 36 | * Create a jar broker from a ServerConfig Object 37 | * 38 | * We do a check to make sure that the ServerConfig contains a valid JarBrokerConfig. 39 | * @param config Server configuration to create a jar broker for 40 | * @param logger Logger ref for log messages 41 | * @return A result containing a jar broker if we were able to create one for the server, else an error 42 | */ 43 | override fun createFromConfig(config: ServerConfig, logger: Logger?): Result { 44 | return when (config.config) { 45 | is JarBrokerConfig -> Result.success(JarBroker(config, logger)) 46 | is CommandBrokerConfig -> Result.success(CommandBroker(config, logger)) 47 | else -> Result.failure(IllegalArgumentException("Invalid configuration for command/jar broker")) 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /api/src/main/kotlin/club/arson/impulse/api/events/ConfigReloadEvent.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Impulse Server Manager for Velocity 3 | * Copyright (c) 2025 Dabb1e 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Affero General Public License as 7 | * published by the Free Software Foundation, either version 3 of the 8 | * License, or (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Affero General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Affero General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package club.arson.impulse.api.events 20 | 21 | import club.arson.impulse.api.config.Configuration 22 | import com.velocitypowered.api.event.ResultedEvent 23 | import com.velocitypowered.api.event.ResultedEvent.GenericResult 24 | 25 | /** 26 | * Event triggered before the Impulse configuration is reloaded. 27 | * 28 | * When impulse detects a change in the configuration file, this event is fired. After resolution, the config manager 29 | * will update its internal state to match the value of "config". You can set the result to "denied" to prevent downstream 30 | * listeners from updating their configuration. 31 | * 32 | * @property config the new configuration 33 | * @property oldConfig the current live configuration 34 | * @property result the result of the event 35 | * @constructor create a new config reload event from and old and new configuration 36 | */ 37 | class ConfigReloadEvent( 38 | var config: Configuration, 39 | var oldConfig: Configuration, 40 | @JvmField var result: GenericResult = GenericResult.allowed() 41 | ) : ResultedEvent { 42 | /** 43 | * Get the current event result 44 | * 45 | * @return the current event result 46 | */ 47 | override fun getResult(): GenericResult { 48 | return result 49 | } 50 | 51 | /** 52 | * Set the event result 53 | * 54 | * @param result the new event result 55 | */ 56 | override fun setResult(result: GenericResult) { 57 | this.result = result 58 | } 59 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/club/arson/impulse/commands/PinServer.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Impulse Server Manager for Velocity 3 | * Copyright (c) 2025 Dabb1e 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Affero General Public License as 7 | * published by the Free Software Foundation, either version 3 of the 8 | * License, or (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Affero General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Affero General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package club.arson.impulse.commands 20 | 21 | import club.arson.impulse.ServiceRegistry 22 | import com.mojang.brigadier.Command 23 | import com.mojang.brigadier.builder.LiteralArgumentBuilder 24 | import com.velocitypowered.api.command.CommandSource 25 | import net.kyori.adventure.text.Component 26 | import net.kyori.adventure.text.format.NamedTextColor 27 | 28 | fun createPinServerCommand(): LiteralArgumentBuilder { 29 | return createGenericServerCommand("pin") { context, serverName -> 30 | val server = ServiceRegistry.instance.serverManager?.getServer(serverName) 31 | if (server != null) { 32 | server.pinned = true 33 | context.source.sendMessage( 34 | Component.text() 35 | .content("Server $serverName ") 36 | .append( 37 | Component.text("pinned") 38 | .color(NamedTextColor.RED) 39 | ) 40 | ) 41 | } else { 42 | context.source.sendMessage( 43 | Component.text() 44 | .append( 45 | Component.text("Error: ") 46 | .color(NamedTextColor.RED) 47 | ) 48 | .append( 49 | Component.text("server $serverName not found") 50 | ) 51 | 52 | ) 53 | } 54 | return@createGenericServerCommand Command.SINGLE_SUCCESS 55 | } 56 | } -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | kotlin = "2.1.20" 3 | dokka = "2.0.0" 4 | docker = "3.5.0" 5 | kotlinxCoroutines = "1.10.2" 6 | shadow = "8.3.6" 7 | velocity = "3.4.0-SNAPSHOT" 8 | kaml = "0.77.1" 9 | classGraph = "4.8.179" 10 | mockk = "1.14.0" 11 | guice = "7.0.0" 12 | jacoco = "0.8.12" 13 | 14 | [libraries] 15 | kotlinStdLib = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin" } 16 | kotlinGradlePlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } 17 | kotlinSerializationPlugin = { module = "org.jetbrains.kotlin.plugin.serialization:org.jetbrains.kotlin.plugin.serialization.gradle.plugin", version.ref = "kotlin" } 18 | kotlinReflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" } 19 | kotlinTestjunit5 = { module = "org.jetbrains.kotlin:kotlin-test-junit5", version.ref = "kotlin" } 20 | dokkaGradlePlugin = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "dokka" } 21 | dockerBase = { module = "com.github.docker-java:docker-java", version.ref = "docker" } 22 | dockerTransport = { module = "com.github.docker-java:docker-java-transport-httpclient5", version.ref = "docker" } 23 | kotlinxCoroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutines" } 24 | shadow = { module = "com.gradleup.shadow:com.gradleup.shadow.gradle.plugin", version.ref = "shadow" } 25 | velocity = { module = "com.velocitypowered:velocity-api", version.ref = "velocity" } 26 | kaml = { module = "com.charleskorn.kaml:kaml", version.ref = "kaml" } 27 | classGraph = { module = "io.github.classgraph:classgraph", version.ref = "classGraph" } 28 | mockk = { module = "io.mockk:mockk", version.ref = "mockk" } 29 | guice = { module = "com.google.inject:guice", version.ref = "guice" } 30 | 31 | [bundles] 32 | docker = ["dockerBase", "dockerTransport"] 33 | test = ["mockk", "guice", "velocity", "kotlinTestjunit5"] 34 | 35 | [plugins] 36 | kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" } 37 | jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } 38 | jacoco = { id = "org.jacoco", version.ref = "jacoco" } 39 | kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } 40 | dokkaPlugin = { id = "org.jetbrains.dokka", version.ref = "dokka" } 41 | dokkaJavadocPlugin = { id = "org.jetbrains.dokka-javadoc", version.ref = "dokka" } -------------------------------------------------------------------------------- /app/src/main/kotlin/club/arson/impulse/commands/UnpinServer.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Impulse Server Manager for Velocity 3 | * Copyright (c) 2025 Dabb1e 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Affero General Public License as 7 | * published by the Free Software Foundation, either version 3 of the 8 | * License, or (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Affero General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Affero General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package club.arson.impulse.commands 20 | 21 | import club.arson.impulse.ServiceRegistry 22 | import com.mojang.brigadier.Command 23 | import com.mojang.brigadier.builder.LiteralArgumentBuilder 24 | import com.velocitypowered.api.command.CommandSource 25 | import net.kyori.adventure.text.Component 26 | import net.kyori.adventure.text.format.NamedTextColor 27 | 28 | fun createUnpinServerCommand(): LiteralArgumentBuilder { 29 | return createGenericServerCommand("unpin") { context, serverName -> 30 | val server = ServiceRegistry.instance.serverManager?.getServer(serverName) 31 | if (server != null) { 32 | server.pinned = false 33 | context.source.sendMessage( 34 | Component.text() 35 | .content("Server $serverName ") 36 | .append( 37 | Component.text("unpinned") 38 | .color(NamedTextColor.GREEN) 39 | ) 40 | ) 41 | } else { 42 | context.source.sendMessage( 43 | Component.text() 44 | .append( 45 | Component.text("Error: ") 46 | .color(NamedTextColor.RED) 47 | ) 48 | .append( 49 | Component.text("server $serverName not found") 50 | ) 51 | 52 | ) 53 | } 54 | return@createGenericServerCommand Command.SINGLE_SUCCESS 55 | } 56 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # User-specific stuff 2 | .idea/ 3 | 4 | *.iml 5 | *.ipr 6 | *.iws 7 | 8 | # IntelliJ 9 | out/ 10 | # mpeltonen/sbt-idea plugin 11 | .idea_modules/ 12 | 13 | # JIRA plugin 14 | atlassian-ide-plugin.xml 15 | 16 | # Compiled class file 17 | *.class 18 | 19 | # Log file 20 | *.log 21 | 22 | # BlueJ files 23 | *.ctxt 24 | 25 | # Package Files # 26 | *.jar 27 | *.war 28 | *.nar 29 | *.ear 30 | *.zip 31 | *.tar.gz 32 | *.rar 33 | 34 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 35 | hs_err_pid* 36 | 37 | *~ 38 | 39 | # temporary files which can be created if a process still has a handle open of a deleted file 40 | .fuse_hidden* 41 | 42 | # KDE directory preferences 43 | .directory 44 | 45 | # Linux trash folder which might appear on any partition or disk 46 | .Trash-* 47 | 48 | # .nfs files are created when an open file is removed but is still being accessed 49 | .nfs* 50 | 51 | # General 52 | .DS_Store 53 | .AppleDouble 54 | .LSOverride 55 | 56 | # Icon must end with two \r 57 | Icon 58 | 59 | # Thumbnails 60 | ._* 61 | 62 | # Files that might appear in the root of a volume 63 | .DocumentRevisions-V100 64 | .fseventsd 65 | .Spotlight-V100 66 | .TemporaryItems 67 | .Trashes 68 | .VolumeIcon.icns 69 | .com.apple.timemachine.donotpresent 70 | 71 | # Directories potentially created on remote AFP share 72 | .AppleDB 73 | .AppleDesktop 74 | Network Trash Folder 75 | Temporary Items 76 | .apdisk 77 | 78 | # Windows thumbnail cache files 79 | Thumbs.db 80 | Thumbs.db:encryptable 81 | ehthumbs.db 82 | ehthumbs_vista.db 83 | 84 | # Dump file 85 | *.stackdump 86 | 87 | # Folder config file 88 | [Dd]esktop.ini 89 | 90 | # Recycle Bin used on file shares 91 | $RECYCLE.BIN/ 92 | 93 | # Windows Installer files 94 | *.cab 95 | *.msi 96 | *.msix 97 | *.msm 98 | *.msp 99 | 100 | # Windows shortcuts 101 | *.lnk 102 | 103 | .gradle 104 | build/ 105 | 106 | # Ignore Gradle GUI config 107 | gradle-app.setting 108 | 109 | # Cache of project 110 | .gradletasknamecache 111 | 112 | **/build/ 113 | 114 | # Common working directory 115 | run/ 116 | runs/ 117 | .kotlin/ 118 | 119 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 120 | !gradle-wrapper.jar 121 | 122 | # Temp ignores untill the files are ready 123 | src/main/java 124 | 125 | # MDBook ignores 126 | book/ 127 | index.html 128 | -------------------------------------------------------------------------------- /app/src/main/kotlin/club/arson/impulse/commands/GenericServerCommand.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Impulse Server Manager for Velocity 3 | * Copyright (c) 2025 Dabb1e 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Affero General Public License as 7 | * published by the Free Software Foundation, either version 3 of the 8 | * License, or (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Affero General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Affero General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package club.arson.impulse.commands 20 | 21 | import club.arson.impulse.ServiceRegistry 22 | import com.mojang.brigadier.arguments.StringArgumentType 23 | import com.mojang.brigadier.builder.LiteralArgumentBuilder 24 | import com.mojang.brigadier.context.CommandContext 25 | import com.mojang.brigadier.suggestion.SuggestionProvider 26 | import com.velocitypowered.api.command.BrigadierCommand 27 | import com.velocitypowered.api.command.CommandSource 28 | 29 | fun serverSuggestionProvider(): SuggestionProvider { 30 | return SuggestionProvider { _, builder -> 31 | val serverNames = ServiceRegistry.instance.configManager?.servers?.map { it.name } ?: emptyList() 32 | serverNames.forEach { serverName -> 33 | builder.suggest(serverName) 34 | } 35 | builder.buildFuture() 36 | } 37 | } 38 | 39 | 40 | fun createGenericServerCommand( 41 | name: String, 42 | task: (CommandContext, String) -> Int 43 | ): LiteralArgumentBuilder { 44 | 45 | return BrigadierCommand.literalArgumentBuilder(name) 46 | .requires { source -> source.hasPermission("impulse.server.$name") } 47 | .then( 48 | BrigadierCommand 49 | .requiredArgumentBuilder("server", StringArgumentType.word()) 50 | .suggests(serverSuggestionProvider()) 51 | .executes { context -> 52 | val serverName = context.getArgument("server", String::class.java) 53 | task(context, serverName) 54 | } 55 | ) 56 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/club/arson/impulse/server/ServerBroker.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Impulse Server Manager for Velocity 3 | * Copyright (c) 2025 Dabb1e 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Affero General Public License as 7 | * published by the Free Software Foundation, either version 3 of the 8 | * License, or (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Affero General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Affero General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package club.arson.impulse.server 20 | 21 | import club.arson.impulse.api.config.ServerConfig 22 | import club.arson.impulse.api.server.Broker 23 | import club.arson.impulse.api.server.BrokerFactory 24 | import com.google.inject.Inject 25 | import org.slf4j.Logger 26 | import kotlin.reflect.KClass 27 | 28 | /** 29 | * A class for managing server broker factories and configuration classes. These should be injected from the [club.arson.impulse.inject.modules.BrokerModule]. 30 | * Guice module. 31 | * 32 | * @property brokers a set of broker factories 33 | * @property configClasses a map of broker types to configuration classes 34 | * @constructor creates a new server broker instance 35 | */ 36 | class ServerBroker @Inject constructor( 37 | private val brokers: Set, 38 | val configClasses: Map> 39 | ) { 40 | /** 41 | * Creates a broker from a given configuration. 42 | * 43 | * Attempts to use the config "type" filed to determine the correct broker factory to forward the request to. 44 | * @param config the server configuration 45 | * @param logger the logger to use for the broker 46 | * @return a result containing the broker or an error 47 | */ 48 | fun createFromConfig(config: ServerConfig, logger: Logger? = null): Result { 49 | val brokerFactory = brokers.find { it.provides.contains(config.type) } 50 | if (brokerFactory == null) { 51 | return Result.failure(IllegalArgumentException("Invalid broker type: ${config.type}")) 52 | } 53 | 54 | return brokerFactory.createFromConfig(config, logger) 55 | } 56 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/club/arson/impulse/inject/providers/RegisteredServerProvider.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Impulse Server Manager for Velocity 3 | * Copyright (c) 2025 Dabb1e 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Affero General Public License as 7 | * published by the Free Software Foundation, either version 3 of the 8 | * License, or (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Affero General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Affero General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package club.arson.impulse.inject.providers 20 | 21 | import club.arson.impulse.api.config.ServerConfig 22 | import club.arson.impulse.api.server.Broker 23 | import com.google.inject.Provider 24 | import com.velocitypowered.api.proxy.ProxyServer 25 | import com.velocitypowered.api.proxy.server.RegisteredServer 26 | import com.velocitypowered.api.proxy.server.ServerInfo 27 | import org.slf4j.Logger 28 | import kotlin.jvm.optionals.getOrNull 29 | 30 | /** 31 | * Provider for registered servers 32 | * 33 | * Returns a registered server if one exists in Velocity. creates one if it does not 34 | * @param proxy reference to the proxy instance 35 | * @param config the server configuration 36 | */ 37 | class RegisteredServerProvider( 38 | private val proxy: ProxyServer, 39 | private val config: ServerConfig, 40 | private val broker: Broker, 41 | private val logger: Logger? = null 42 | ) : 43 | Provider { 44 | /** 45 | * Gets a registered server if it exists for the provided server name 46 | * 47 | * @return the existing server instance or a newly created one 48 | */ 49 | override fun get(): RegisteredServer { 50 | val existingServer = proxy.getServer(config.name).getOrNull() 51 | if (existingServer != null) { 52 | return existingServer 53 | } 54 | val address = broker.address().getOrNull() 55 | if (address == null) { 56 | logger?.error("Failed to get address for server ${config.name}") 57 | throw IllegalStateException("Failed to get address for server ${config.name}") 58 | } 59 | return proxy.registerServer(ServerInfo(config.name, address)) 60 | } 61 | } -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Impulse Server Manager for Velocity 3 | * Copyright (c) 2025 Dabb1e 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Affero General Public License as 7 | * published by the Free Software Foundation, either version 3 of the 8 | * License, or (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Affero General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Affero General Public License 16 | * along with this program. If not, see . 17 | */ 18 | import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar 19 | 20 | plugins { 21 | conventions.`impulse-base` 22 | conventions.`impulse-publish` 23 | conventions.`shadow-jar` 24 | 25 | id("eclipse") 26 | } 27 | 28 | group = "club.arson.impulse" 29 | 30 | dependencies { 31 | implementation(libs.kotlinReflect) 32 | implementation(libs.kotlinStdLib) 33 | implementation(libs.classGraph) 34 | implementation(project(":api")) 35 | 36 | testImplementation(project(":api")) 37 | } 38 | 39 | val templateSource = file("src/main/templates") 40 | val templateDest = layout.buildDirectory.dir("generated/sources/templates") 41 | val generateTemplates = tasks.register("generateTemplates") { 42 | val props = mapOf("version" to project.version) 43 | inputs.properties(props) 44 | 45 | from(templateSource) 46 | into(templateDest) 47 | expand(props) 48 | } 49 | 50 | sourceSets.main.configure { java.srcDir(generateTemplates.map { it.outputs }) } 51 | 52 | project.eclipse.synchronizationTasks(generateTemplates) 53 | 54 | tasks.withType().configureEach { 55 | description = "Lite distribution of Impulse without any bundled brokers." 56 | relocate("org.jetbrains.kotlin", "club.arson.impulse.kotlin") 57 | relocate("io.github.classgraph", "club.arson.impulse.classgraph") 58 | archiveBaseName = "impulse-lite" 59 | } 60 | 61 | tasks.withType().configureEach { 62 | useJUnitPlatform() 63 | dependsOn(":api:jar") 64 | } 65 | 66 | impulsePublish { 67 | artifact = tasks.named("shadowJar").get() 68 | description = "Lite distribution of Impulse without any bundled brokers." 69 | licenses = listOf( 70 | kamlLicense, 71 | impulseLicense, 72 | classGraphLicense 73 | ) 74 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/club/arson/impulse/inject/providers/BrokerFactoryProvider.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Impulse Server Manager for Velocity 3 | * Copyright (c) 2025 Dabb1e 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Affero General Public License as 7 | * published by the Free Software Foundation, either version 3 of the 8 | * License, or (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Affero General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Affero General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package club.arson.impulse.inject.providers 20 | 21 | import club.arson.impulse.api.server.BrokerFactory 22 | import com.google.inject.Provider 23 | import io.github.classgraph.ClassGraph 24 | import org.slf4j.Logger 25 | 26 | /** 27 | * Provider for all BrokerFactory implementations found on the classpath 28 | * 29 | * @property logger the logger to use for messages 30 | */ 31 | class BrokerFactoryProvider(private val logger: Logger? = null) : 32 | Provider> { 33 | /** 34 | * Scans the classpath for all implementations of [BrokerFactory] 35 | * 36 | * @return a set of all found implementations 37 | */ 38 | override fun get(): Set { 39 | val implementations = mutableSetOf() 40 | ClassGraph() 41 | .enableClassInfo() 42 | .acceptPackages() 43 | .scan() 44 | .use { scanResult -> 45 | scanResult 46 | .allClasses 47 | .filter { c -> c.implementsInterface(BrokerFactory::class.java.name) } 48 | .forEach { classInfo -> 49 | runCatching { 50 | val c = classInfo.loadClass(BrokerFactory::class.java) 51 | implementations.add(c.getDeclaredConstructor().newInstance()) 52 | } 53 | .onSuccess { logger?.info("BrokerProvider: Found Broker ${classInfo.name} on path") } 54 | .onFailure { logger?.warn("Found Broker ${classInfo.name} on path, but failed in instantiate: ${it.message}") } 55 | } 56 | } 57 | return implementations 58 | } 59 | } -------------------------------------------------------------------------------- /api/src/main/kotlin/club/arson/impulse/api/server/Broker.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Impulse Server Manager for Velocity 3 | * Copyright (c) 2025 Dabb1e 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Affero General Public License as 7 | * published by the Free Software Foundation, either version 3 of the 8 | * License, or (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Affero General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Affero General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package club.arson.impulse.api.server 20 | 21 | import club.arson.impulse.api.config.ServerConfig 22 | import java.net.InetSocketAddress 23 | 24 | /** 25 | * Common interface for server brokers. 26 | */ 27 | interface Broker { 28 | /** 29 | * Should return the current [Status] of the server 30 | */ 31 | fun getStatus(): Status 32 | 33 | /** 34 | * Should return the address used to connect players to the server. 35 | * 36 | * This is only used when the server is dynamically registered with Velocity. 37 | */ 38 | fun address(): Result 39 | 40 | /** 41 | * Should return true if the server is running, false otherwise. 42 | */ 43 | fun isRunning(): Boolean 44 | 45 | /** 46 | * Should start the server, and kick off any actions needed to get it ready to accept players. 47 | */ 48 | fun startServer(): Result 49 | 50 | /** 51 | * Should stop the server. 52 | * 53 | * Semantically, this should "pause" the server leaving the resources in a state that can be resumed. If this does 54 | * not make sense for the broker type then it should act the same as "remove". 55 | */ 56 | fun stopServer(): Result 57 | 58 | /** 59 | * Should remove the server. 60 | * 61 | * This should stop the server and clean up any runtime resources. It should NOT delete any persistent data volumes. 62 | */ 63 | fun removeServer(): Result 64 | 65 | /** 66 | * Trigger a reconciliation to resolve any differences between the server configuration and the runtime state of 67 | * the server. 68 | * 69 | * This should return a [Runnable] that can be used to perform the reconciliation if there is no work to be done. 70 | */ 71 | fun reconcile(config: ServerConfig): Result 72 | } -------------------------------------------------------------------------------- /theme/catppuccin-alerts.css: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2021 Catppuccin 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | .latte .mdbook-alerts-note { 25 | --mdbook-alerts-color: #1e66f5; 26 | } 27 | .latte .mdbook-alerts-tip { 28 | --mdbook-alerts-color: #40a02b; 29 | } 30 | .latte .mdbook-alerts-important { 31 | --mdbook-alerts-color: #8839ef; 32 | } 33 | .latte .mdbook-alerts-warning { 34 | --mdbook-alerts-color: #df8e1d; 35 | } 36 | .latte .mdbook-alerts-caution { 37 | --mdbook-alerts-color: #d20f39; 38 | } 39 | 40 | .frappe .mdbook-alerts-note { 41 | --mdbook-alerts-color: #8caaee; 42 | } 43 | .frappe .mdbook-alerts-tip { 44 | --mdbook-alerts-color: #a6d189; 45 | } 46 | .frappe .mdbook-alerts-important { 47 | --mdbook-alerts-color: #ca9ee6; 48 | } 49 | .frappe .mdbook-alerts-warning { 50 | --mdbook-alerts-color: #e5c890; 51 | } 52 | .frappe .mdbook-alerts-caution { 53 | --mdbook-alerts-color: #e78284; 54 | } 55 | 56 | .macchiato .mdbook-alerts-note { 57 | --mdbook-alerts-color: #8aadf4; 58 | } 59 | .macchiato .mdbook-alerts-tip { 60 | --mdbook-alerts-color: #a6da95; 61 | } 62 | .macchiato .mdbook-alerts-important { 63 | --mdbook-alerts-color: #c6a0f6; 64 | } 65 | .macchiato .mdbook-alerts-warning { 66 | --mdbook-alerts-color: #eed49f; 67 | } 68 | .macchiato .mdbook-alerts-caution { 69 | --mdbook-alerts-color: #ed8796; 70 | } 71 | 72 | .mocha .mdbook-alerts-note { 73 | --mdbook-alerts-color: #89b4fa; 74 | } 75 | .mocha .mdbook-alerts-tip { 76 | --mdbook-alerts-color: #a6e3a1; 77 | } 78 | .mocha .mdbook-alerts-important { 79 | --mdbook-alerts-color: #cba6f7; 80 | } 81 | .mocha .mdbook-alerts-warning { 82 | --mdbook-alerts-color: #f9e2af; 83 | } 84 | .mocha .mdbook-alerts-caution { 85 | --mdbook-alerts-color: #f38ba8; 86 | } 87 | -------------------------------------------------------------------------------- /app/src/main/kotlin/club/arson/impulse/inject/providers/BrokerConfigProvider.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Impulse Server Manager for Velocity 3 | * Copyright (c) 2025 Dabb1e 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Affero General Public License as 7 | * published by the Free Software Foundation, either version 3 of the 8 | * License, or (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Affero General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Affero General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package club.arson.impulse.inject.providers 20 | 21 | import club.arson.impulse.api.config.BrokerConfig 22 | import com.google.inject.Provider 23 | import io.github.classgraph.ClassGraph 24 | import org.slf4j.Logger 25 | import kotlin.reflect.KClass 26 | 27 | /** 28 | * Guice provider for Broker configuration classes 29 | * 30 | * Broker configuration classes do not have a set interface, so we scan for the [BrokerConfig] annotation. If found we 31 | * use the associated brokerId to register the class to a specific Broker type. 32 | * @property logger the logger to use for log messages 33 | */ 34 | class BrokerConfigProvider(private val logger: Logger? = null) : 35 | Provider>> { 36 | /** 37 | * Scans the classpath for classes annotated with [BrokerConfig] and returns a map of brokerId to class 38 | * @return a map of brokerId to class 39 | */ 40 | override fun get(): Map> { 41 | val implementations = mutableMapOf>() 42 | ClassGraph() 43 | .enableAnnotationInfo() 44 | .enableClassInfo() 45 | .acceptPackages() 46 | .scan() 47 | .use { scanResult -> 48 | scanResult 49 | .getClassesWithAnnotation(BrokerConfig::class.java.name) 50 | .forEach { classInfo -> 51 | var brokerId = "" 52 | runCatching { 53 | val clazz = classInfo.loadClass() 54 | brokerId = clazz.getAnnotation(BrokerConfig::class.java).brokerId 55 | implementations.put(brokerId, clazz.kotlin as KClass) 56 | } 57 | .onSuccess { logger?.debug("BrokerConfigProver: registered config for $brokerId") } 58 | .onFailure { logger?.warn("BrokerConfigProvider: Found config for broker $brokerId but unable to register it: ${it.message}") } 59 | } 60 | } 61 | return implementations 62 | } 63 | } -------------------------------------------------------------------------------- /api/src/main/kotlin/club/arson/impulse/api/config/LifecycleSettings.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Impulse Server Manager for Velocity 3 | * Copyright (c) 2025 Dabb1e 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Affero General Public License as 7 | * published by the Free Software Foundation, either version 3 of the 8 | * License, or (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Affero General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Affero General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package club.arson.impulse.api.config 20 | 21 | import club.arson.impulse.api.config.ReconcileBehavior.FORCE 22 | import club.arson.impulse.api.config.ReconcileBehavior.ON_STOP 23 | import kotlinx.serialization.Serializable 24 | 25 | /** 26 | * Lifecycle settings for a server 27 | * 28 | * These are settings that are used to control the basic lifecycle of a server. This allows a user to override the 29 | * default behavior of the server manager. 30 | * @property timeouts Various timeouts used for the server 31 | * @property allowAutoStart Whether the server should be allowed to start automatically 32 | * @property allowAutoStop Whether the server should be allowed to stop automatically 33 | * @property shutdownBehavior The action to take when the server receives a shutdown signal. Either "STOP" or "REMOVE" 34 | */ 35 | @Serializable 36 | data class LifecycleSettings( 37 | var timeouts: Timeouts = Timeouts(), 38 | var allowAutoStart: Boolean = true, 39 | var allowAutoStop: Boolean = true, 40 | var reconciliationBehavior: ReconcileBehavior = ON_STOP, 41 | var shutdownBehavior: ShutdownBehavior = ShutdownBehavior.STOP 42 | ) 43 | 44 | /** 45 | * Various timeouts used for the server 46 | * 47 | * @property startup The time in seconds before a server is considered to have failed to start 48 | * @property shutdown The time in seconds before a server is considered to have failed to stop 49 | * @property reconciliationGracePeriod The time in seconds to wait before reconciling servers if 50 | * [LifecycleSettings.reconciliationBehavior] is set to [ReconcileBehavior.FORCE] 51 | * @property inactiveGracePeriod The time in seconds to wait before shutting down an inactive servers (no players) 52 | */ 53 | @Serializable 54 | data class Timeouts( 55 | var startup: Long = 120, 56 | var shutdown: Long = 120, 57 | var reconciliationGracePeriod: Long = 60, 58 | var inactiveGracePeriod: Long = 300, 59 | ) 60 | 61 | /** 62 | * The behavior to take when reconciling a server 63 | * @property FORCE Reconcile the server immediately 64 | * @property ON_STOP Reconcile the server on the next stop 65 | */ 66 | @Serializable 67 | enum class ReconcileBehavior { 68 | FORCE, 69 | ON_STOP 70 | } -------------------------------------------------------------------------------- /app/src/test/kotlin/club/arson/impulse/ServiceRegistryTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Impulse Server Manager for Velocity 3 | * Copyright (c) 2025 Dabb1e 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Affero General Public License as 7 | * published by the Free Software Foundation, either version 3 of the 8 | * License, or (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Affero General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Affero General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package club.arson.impulse 20 | 21 | import club.arson.impulse.config.ConfigManager 22 | import club.arson.impulse.server.ServerBroker 23 | import club.arson.impulse.server.ServerManager 24 | import com.google.inject.Injector 25 | import io.mockk.every 26 | import io.mockk.mockk 27 | import io.mockk.verify 28 | import org.junit.jupiter.api.BeforeEach 29 | import org.junit.jupiter.api.Test 30 | import org.junit.jupiter.api.assertThrows 31 | 32 | class ServiceRegistryTest { 33 | private lateinit var serverManager: ServerManager 34 | private lateinit var configManager: ConfigManager 35 | private lateinit var brokerInjector: Injector 36 | 37 | @BeforeEach 38 | fun setUp() { 39 | ServiceRegistry.instance.reset() 40 | serverManager = mockk() 41 | configManager = mockk() 42 | brokerInjector = mockk() 43 | } 44 | 45 | @Test 46 | fun testNullOnInit() { 47 | val registry = ServiceRegistry.instance 48 | assert(registry.serverManager == null) 49 | assert(registry.configManager == null) 50 | assert(registry.injector == null) 51 | } 52 | 53 | @Test 54 | fun testPreventDoubleRegistration() { 55 | val registry = ServiceRegistry.instance 56 | registry.serverManager = serverManager 57 | assertThrows { 58 | registry.serverManager = serverManager 59 | } 60 | registry.configManager = configManager 61 | assertThrows { 62 | registry.configManager = configManager 63 | } 64 | registry.injector = brokerInjector 65 | assertThrows { 66 | registry.injector = brokerInjector 67 | } 68 | } 69 | 70 | @Test 71 | fun testGetServerBroker() { 72 | val registry = ServiceRegistry.instance 73 | assertThrows { 74 | registry.getServerBroker() 75 | } 76 | 77 | every { brokerInjector.getInstance(ServerBroker::class.java) } returns mockk() 78 | registry.injector = brokerInjector 79 | registry.getServerBroker() 80 | verify(exactly = 1) { brokerInjector.getInstance(ServerBroker::class.java) } 81 | } 82 | } -------------------------------------------------------------------------------- /command-broker/src/main/kotlin/club/arson/impulse/commandbroker/JarBroker.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Impulse Server Manager for Velocity 3 | * Copyright (c) 2025 Dabb1e 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Affero General Public License as 7 | * published by the Free Software Foundation, either version 3 of the 8 | * License, or (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Affero General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Affero General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package club.arson.impulse.commandbroker 20 | 21 | import club.arson.impulse.api.config.ServerConfig 22 | import club.arson.impulse.api.server.Broker 23 | import club.arson.impulse.api.server.Status 24 | import org.slf4j.Logger 25 | import java.net.InetSocketAddress 26 | 27 | /** 28 | * This broker is designed to run raw jar files. The resulting PID is then managed as if it were a server. It is a 29 | * wrapper around the CommandBroker. 30 | */ 31 | class JarBroker(serverConfig: ServerConfig, logger: Logger? = null) : Broker { 32 | private val commandBroker: CommandBroker 33 | 34 | init { 35 | val cmdConfig = toCommandBrokerConfig(serverConfig.config as JarBrokerConfig) 36 | serverConfig.config = cmdConfig 37 | commandBroker = CommandBroker(serverConfig, logger) 38 | } 39 | 40 | override fun address(): Result = commandBroker.address() 41 | 42 | override fun isRunning(): Boolean = commandBroker.isRunning() 43 | 44 | override fun getStatus(): Status = commandBroker.getStatus() 45 | 46 | override fun startServer(): Result = commandBroker.startServer() 47 | 48 | override fun stopServer(): Result = commandBroker.stopServer() 49 | 50 | override fun removeServer(): Result = commandBroker.removeServer() 51 | 52 | /** 53 | * Reconcile any changes to our configuration. 54 | * 55 | * We need to do some minor validation before converting our JarBrokerConfig to a CommandBrokerConfig. After that, 56 | * we call our parent reconcile method with the converted config. 57 | * @param config Server configuration to reconcile 58 | * @return A result containing a runnable if we were able to reconcile the configuration, else an error 59 | */ 60 | override fun reconcile(config: ServerConfig): Result { 61 | if (config.type != "jar") { 62 | return Result.failure(IllegalArgumentException("Expected JarBrokerConfig and got something else!")) 63 | } 64 | 65 | val cmdConfig = toCommandBrokerConfig(config.config as JarBrokerConfig) 66 | config.config = cmdConfig 67 | config.type = "cmd" 68 | 69 | return commandBroker.reconcile(config) 70 | } 71 | } -------------------------------------------------------------------------------- /app/src/test/kotlin/club/arson/impulse/commands/GenericServerCommandTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Impulse Server Manager for Velocity 3 | * Copyright (c) 2025 Dabb1e 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Affero General Public License as 7 | * published by the Free Software Foundation, either version 3 of the 8 | * License, or (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Affero General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Affero General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package club.arson.impulse.commands 20 | 21 | import club.arson.impulse.ServiceRegistry 22 | import club.arson.impulse.api.config.ServerConfig 23 | import club.arson.impulse.config.ConfigManager 24 | import com.mojang.brigadier.suggestion.SuggestionsBuilder 25 | import io.mockk.every 26 | import io.mockk.mockk 27 | import io.mockk.verify 28 | import org.junit.jupiter.api.BeforeEach 29 | import org.junit.jupiter.api.Test 30 | 31 | class CommandTests { 32 | @BeforeEach 33 | fun setUp() { 34 | ServiceRegistry.instance.reset() 35 | } 36 | 37 | @Test 38 | fun testServerSuggestionsProvider() { 39 | val mockConfigManager = mockk() 40 | val mockServer = mockk() 41 | 42 | every { mockServer.name } returns "Server1" 43 | every { mockConfigManager.servers } returns listOf(mockServer) 44 | ServiceRegistry.instance.configManager = mockConfigManager 45 | 46 | val suggestionProvider = serverSuggestionProvider() 47 | val mockBuilder = mockk(relaxed = true) 48 | 49 | suggestionProvider.getSuggestions(mockk(), mockBuilder) 50 | verify { mockBuilder.suggest("Server1") } 51 | } 52 | 53 | @Test 54 | fun testServerSuggestionsProviderEmpty() { 55 | val mockConfigManager = mockk() 56 | every { mockConfigManager.servers } returns emptyList() 57 | ServiceRegistry.instance.configManager = mockConfigManager 58 | 59 | val suggestionProvider = serverSuggestionProvider() 60 | val mockBuilder = mockk(relaxed = true) 61 | 62 | suggestionProvider.getSuggestions(mockk(), mockBuilder) 63 | verify(exactly = 0) { mockBuilder.suggest(any()) } 64 | verify(exactly = 0) { mockBuilder.suggest(any()) } 65 | } 66 | 67 | @Test 68 | fun testServerSuggestionsProviderNoConfigManager() { 69 | ServiceRegistry.instance.configManager = null 70 | val suggestionProvider = serverSuggestionProvider() 71 | val mockBuilder = mockk(relaxed = true) 72 | 73 | suggestionProvider.getSuggestions(mockk(), mockBuilder) 74 | verify(exactly = 0) { mockBuilder.suggest(any()) } 75 | verify(exactly = 0) { mockBuilder.suggest(any()) } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /app/src/main/kotlin/club/arson/impulse/ServiceRegistry.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Impulse Server Manager for Velocity 3 | * Copyright (c) 2025 Dabb1e 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Affero General Public License as 7 | * published by the Free Software Foundation, either version 3 of the 8 | * License, or (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Affero General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Affero General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package club.arson.impulse 20 | 21 | import club.arson.impulse.config.ConfigManager 22 | import club.arson.impulse.server.ServerBroker 23 | import club.arson.impulse.server.ServerManager 24 | import com.google.inject.Injector 25 | 26 | /** 27 | * Singleton registry for managing "global" resources 28 | * 29 | * This should be initialized by the Proxy on startup and as early as possible. 30 | */ 31 | class ServiceRegistry { 32 | /** 33 | * Holds the singleton instance for the parent class 34 | */ 35 | companion object { 36 | /** 37 | * Singleton instance of the [ServiceRegistry] 38 | */ 39 | val instance: ServiceRegistry by lazy { ServiceRegistry() } 40 | } 41 | 42 | /** 43 | * The [ServerManager] instance used to interact with managed servers. 44 | * 45 | * @throws IllegalStateException if the [ServerManager] is already registered 46 | */ 47 | var serverManager: ServerManager? = null 48 | set(value) { 49 | if (value != null && serverManager != null) { 50 | throw IllegalStateException("ServerManager already registered") 51 | } 52 | field = value 53 | } 54 | 55 | /** 56 | * The [ConfigManager] instance used to interact with the configuration. 57 | * 58 | * @throws IllegalStateException if the [ConfigManager] is already registered 59 | */ 60 | var configManager: ConfigManager? = null 61 | set(value) { 62 | if (value != null && configManager != null) { 63 | throw IllegalStateException("ConfigManager already registered") 64 | } 65 | field = value 66 | } 67 | 68 | /** 69 | * An [com.google.inject.Injector] instance used for retrieving available Brokers 70 | * 71 | * We use the Guice framework to dynamically inject Brokers. This allows you to access the set of currently 72 | * registered Brokers and Broker Configs. 73 | */ 74 | var injector: Injector? = null 75 | set(value) { 76 | if (value != null && injector != null) { 77 | throw IllegalStateException("BrokerInjector already registered") 78 | } 79 | field = value 80 | } 81 | 82 | /** 83 | * Creates a new [ServerBroker] instance from the registered [Injector] 84 | * @return a new [ServerBroker] instance 85 | */ 86 | fun getServerBroker(): ServerBroker { 87 | return injector?.getInstance(ServerBroker::class.java) 88 | ?: throw IllegalStateException("BrokerInjector not registered") 89 | } 90 | 91 | /** 92 | * Resets the registry to allow for reinitialization 93 | */ 94 | fun reset() { 95 | serverManager = null 96 | configManager = null 97 | injector = null 98 | } 99 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/club/arson/impulse/commands/ServerStatus.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Impulse Server Manager for Velocity 3 | * Copyright (c) 2025 Dabb1e 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Affero General Public License as 7 | * published by the Free Software Foundation, either version 3 of the 8 | * License, or (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Affero General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Affero General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package club.arson.impulse.commands 20 | 21 | import club.arson.impulse.ServiceRegistry 22 | import club.arson.impulse.api.server.Status 23 | import club.arson.impulse.server.Server 24 | import com.mojang.brigadier.Command 25 | import com.mojang.brigadier.arguments.StringArgumentType 26 | import com.mojang.brigadier.builder.LiteralArgumentBuilder 27 | import com.velocitypowered.api.command.BrigadierCommand 28 | import com.velocitypowered.api.command.CommandSource 29 | import net.kyori.adventure.text.Component 30 | import net.kyori.adventure.text.format.NamedTextColor 31 | 32 | fun getServerStatusMessage(name: String, status: Status, padTo: Int = 0): Component { 33 | return Component.text() 34 | .content("$name: ".padEnd(padTo + 2)) 35 | .append( 36 | Component.text(status.toString()) 37 | .color( 38 | when (status) { 39 | Status.RUNNING -> NamedTextColor.GREEN 40 | Status.STOPPED -> NamedTextColor.GRAY 41 | Status.REMOVED -> NamedTextColor.YELLOW 42 | Status.UNKNOWN -> NamedTextColor.RED 43 | } 44 | ) 45 | ) 46 | .build() 47 | } 48 | 49 | fun serverStatusTable(servers: List): Component { 50 | val table = Component.text() 51 | .content("Server Status") 52 | var maxLength = 0; 53 | if (servers.isNotEmpty()) { 54 | maxLength = servers.maxOf { it.config.name.length } 55 | } 56 | 57 | servers.forEach { server -> 58 | table.appendNewline().append(getServerStatusMessage(server.config.name, server.getStatus(), maxLength)) 59 | } 60 | 61 | return table.build() 62 | } 63 | 64 | fun createServerStatusCommand(): LiteralArgumentBuilder { 65 | return BrigadierCommand.literalArgumentBuilder("status") 66 | .requires { source -> source.hasPermission("impulse.server.status") } 67 | .executes { context -> 68 | val table = 69 | serverStatusTable(ServiceRegistry.instance.serverManager?.servers?.values?.toList() ?: emptyList()) 70 | context.source.sendMessage(table) 71 | return@executes Command.SINGLE_SUCCESS 72 | } 73 | .then( 74 | BrigadierCommand 75 | .requiredArgumentBuilder("server", StringArgumentType.word()) 76 | .suggests(serverSuggestionProvider()) 77 | .executes { context -> 78 | val serverName = context.getArgument("server", String::class.java) 79 | val server = ServiceRegistry.instance.serverManager?.getServer(serverName) 80 | if (server != null) { 81 | val table = serverStatusTable(listOf(server)) 82 | context.source.sendMessage(table) 83 | } else { 84 | val message = Component 85 | .text() 86 | .content("Server not found $serverName") 87 | .build() 88 | context.source.sendMessage(message) 89 | } 90 | return@executes Command.SINGLE_SUCCESS 91 | }) 92 | } 93 | -------------------------------------------------------------------------------- /docs/getting_started/connecting_and_exploring.md: -------------------------------------------------------------------------------- 1 | # Connecting and Exploring 2 | 3 | Velocity is set up to hot reload its configuration when it changes. This means you should not have to restart Velocity. 4 | You can verify that your configuration is loaded by looking for the following log message in the Velocity log: 5 | 6 | ``` 7 | [18:52:15 INFO] [Impulse]: Configuration reloaded 8 | [18:52:16 INFO] [Impulse]: ServerManager: server smp reconciled 9 | ``` 10 | 11 | ## Connecting to the server 12 | 13 | If you see this then you should be all set to connect to velocity as normal! When connecting you should notice that you 14 | stay on the join screen for longer than normal. This is because Impulse is starting the server in the background. Once 15 | the server is started you will be connected to it automatically. 16 | 17 | | ![join screen](../assets/images/joining.png) | 18 | |:------------------------------------------------------------------:| 19 | | *The join screen will look like this while the server is starting* | 20 | 21 | If the server takes longer to start than your configured timeout you will be disconnected with an error message. The 22 | server will still attempt to start in the background. Try and reconnect after a few seconds. If the error persists see 23 | the [debugging](#debugging) section. 24 | 25 | | ![error screen](../assets/images/error.png) | 26 | |:------------------------------------------------------:| 27 | | *Error screen if the server start exceeds the timeout* | 28 | 29 | All connections after the first "cold" start should be much faster as the server is already running. Only the first 30 | player to connect after the server has stopped will experience the delay. 31 | 32 | ## Trying out the commands 33 | 34 | > [!IMPORTANT] 35 | > These commands will work from both in game and the server console. If you want to follow along in game, you will need 36 | > to install a command management plugin like [LuckPerms](https://luckperms.net/) into velocity and grant yourself the 37 | > `impulse.server.*` permission. 38 | > ***You must use a command manager to access these in game. Being OP is not enough.*** 39 | 40 | Now that we are on the server lets take a look at some of the commands that Impulse provides. The full command list and 41 | documentation can be found [here](../reference/commands.md). 42 | 43 | ### `impulse status` 44 | 45 | If you run the status command it will give you the current status of all servers that Impulse is managing. You can also 46 | request the status of a specific server by passing the server name as an argument. 47 | 48 | ### `impulse start` 49 | 50 | This command will start a server that is currently stopped. If the server is already running it will do nothing. 51 | 52 | ### `impulse stop` 53 | 54 | This command will stop a server that is currently running. This includes the server that you are currently on. 55 | 56 | ## Leaving the server 57 | 58 | When you are done exploring the server you can disconnect. If you are the only player on the server Impulse will stop 59 | the server after the configured timeout. Be default this is 5 minutes. You can see this in the logs when a server is 60 | stopped: 61 | 62 | ``` 63 | [05:19:04 INFO] [impulse]: Server smp has no players, stopping 64 | ``` 65 | 66 | ## Debugging 67 | 68 | If you are using the docker broker you can check the container status by running the following command: 69 | 70 | ```shell 71 | docker ps -a 72 | ``` 73 | 74 | you should see a container with the name `smp` and the status `Up`. If the status is `Exited` then the server has 75 | crashed for some reason. If the docker status is Up, but you are still having issues connecting to the server it may 76 | still be starting. For either case you can check the logs of the container by running: 77 | 78 | ```shell 79 | docker logs smp 80 | ``` 81 | 82 | If using the JAR broker you can check the log outputs directly for any errors. 83 | 84 | Look for any errors in the logs, or wait for the server to finish starting. If you are still having issues feel free to 85 | open an issue on the [GitHub repository](https://github.com/Arson-Club/Impulse/issues) 86 | 87 | At this point you should be able to continue to set up your server as you see fit. -------------------------------------------------------------------------------- /docs/getting_started/jar_broker.md: -------------------------------------------------------------------------------- 1 | # JAR 2 | 3 | The JAR broker is simple to configure. For our SMP server the following will provide a minimal working configuration. 4 | 5 | ### `plugins/impulse/config.yaml` 6 | 7 | ```yaml 8 | instanceName: MyCoolSMP 9 | servers: 10 | - name: smp 11 | type: jar 12 | lifecycleSettings: 13 | timeouts: 14 | inactiveGracePeriod: 300 15 | jar: 16 | workingDirectory: /srv/smp 17 | jarFile: fabric.jar 18 | javaFlags: 19 | - -Xms4G 20 | - -Xmx4G 21 | flags: 22 | - --nogui 23 | ``` 24 | 25 | Let's break that down a bit! 26 | 27 | ## `instanceName` 28 | 29 | Here we give a unique identifier to our Velocity server. This is not used by the JAR broker, but others may use it to 30 | identify different Velocity instances that they are managing servers for. 31 | 32 | ## `servers` section 33 | 34 | This is where we can define a list of servers that we want Impulse to manage. In this case, we are defining a single 35 | server named `smp`. 36 | 37 | `name` provides a unique identifier for the server. 38 | 39 | > [!IMPORTANT] 40 | > The name of the server in the `servers` section must match the name of the server Velocity's `velocity.toml` 41 | > ***exactly***. If it does not you will get unexpected behaviour! 42 | 43 | `lifecycleSettings` is where we can set all the config properties related to how a server runs. For our SMP server we 44 | are setting the amount of time the server will wait before shutting down after the last player has left to 5 min. 45 | 46 | `type` specifies which broker is managing this server. In this case, we are using the `jar` broker. 47 | 48 | ### `jar` section 49 | 50 | This section contains all the configuration specific to the JAR broker. If you are familiar with running a Minecraft 51 | servers from the command line this may look familiar. 52 | 53 | `workingDirectory` specifies the directory that the server will be run from. This should be the directory that contains 54 | the server jar. 55 | 56 | `jarFile` specifies the server jar file to run. 57 | 58 | `javaFlags` allows us to specify flags to pass to the JVM when starting the server. In this case, we are setting the max 59 | and min heap size to 4G. 60 | 61 | `flags` allows us to specify flags to pass to the server jar. In this case, we are telling the server to run in nogui 62 | mode. 63 | 64 | This configuration will cause the server to be run as if you had run the following command from the `/srv/smp` 65 | directory: 66 | 67 | ```shell 68 | java -Xms4G -Xmx4G -jar fabric.jar --nogui 69 | ``` 70 | 71 | ### Configuring the Server 72 | 73 | > [!TIP] 74 | > If you have not started the server before, run it once manually so it can generates its associated files. 75 | 76 | We will need to handle some configuration of our server if we want it to be able to connect to our Velocity proxy. 77 | 78 | #### `server.properties` 79 | 80 | Because Velocity is running on port `25565` we will need to change the server port in the `server.properties` file to 81 | `25566`. 82 | 83 | ```properties 84 | server-port=25566 85 | ``` 86 | 87 | #### `eula.txt` 88 | 89 | You will also need to agree to the Minecraft EULA. Open the `eula.txt` file and change `eula=false` to `eula=true`. 90 | 91 | ```properties 92 | eula=true 93 | ``` 94 | 95 | #### Identity Forwarding 96 | 97 | Since Velocity is set up with "modern" forwarding and in online mode, we need a bit of configuration on our server 98 | before players will be able to connect. Luckily this is very simple! We just need to add a file to the `/srv/smp` and 99 | install a mod. 100 | 101 | First download the [FabricProxy-Lite](https://modrinth.com/mod/fabricproxy-lite) mod and place it in your mods folder. 102 | Once that is installed you can add the following file to the `/srv/smp/config` directory. 103 | 104 | `/srv/smp/config/FabricProxy-Lite.toml` 105 | 106 | ```toml 107 | hackOnlineMode = true 108 | hackEarlySend = false 109 | hackMessageChain = true 110 | disconnectMessage = "This server requires you to connect with Velocity." 111 | secret = "" 112 | ``` 113 | 114 | You can find the `forwarding.secret` file next to your `velocity.toml`. -------------------------------------------------------------------------------- /docs/getting_started/docker_broker.md: -------------------------------------------------------------------------------- 1 | # Docker 2 | 3 | To configure our SMP server to be managed by Docker, we simply need to add a few lines to our `config.yaml` file. 4 | 5 | ### `plugins/impulse/config.yaml` 6 | 7 | ```yaml 8 | instanceName: MyCoolSMP 9 | servers: 10 | - name: smp 11 | type: docker 12 | lifecycleSettings: 13 | timeouts: 14 | inactiveGracePeriod: 300 15 | docker: 16 | image: "itzg/minecraft-server:latest" 17 | portBindings: 18 | - "25566:25565" 19 | env: 20 | ONLINE_MODE: "FALSE" 21 | TYPE: "FABRIC" 22 | EULA: "TRUE" 23 | MODRINTH_PROJECTS: "fabricproxy-lite" 24 | volumes: 25 | - "/srv/smp:/data" 26 | ``` 27 | 28 | Let's break that down a bit! 29 | 30 | ## `servers` section 31 | 32 | This is where we can define a list of servers that we want Impulse to manage. In this case, we are defining a single 33 | server named `smp`. 34 | 35 | `name` provides a unique identifier for the server. 36 | > [!IMPORTANT] 37 | > The name of the server in the `servers` section must match the name of the server Velocity's `velocity.toml` 38 | ***exactly***. If it does not you will get unexpected behaviour! 39 | 40 | `lifecycleSettings` is where we can set all the config properties related to how a server runs. For our SMP server we 41 | are setting the amount of time the server will wait before shutting down after the last player has left to 5 min. 42 | 43 | `type` specifies which broker is managing this server. In this case, we are using the `docker` broker. 44 | 45 | ### `docker` section 46 | 47 | This section contains all the configuration specific to the Docker broker. If you are familiar with Docker Compose or 48 | Kubernetes many of these options may look familiar. If not, never fear! We will explain them all. 49 | 50 | `image` specifies the Docker image that we want to use for this server. In this case, we are using the 51 | `itzg/minecraft-server` image. This image is a very popular image for running Minecraft servers in Docker containers and 52 | is the default. Documentation can be found [here](https://docker-minecraft-server.readthedocs.io/en/latest/). Impulse 53 | will work with any image, so feel free to change this as needed. 54 | 55 | `portBindings` allows us to define how we want to map ports from the host to the container. In this case, we are mapping 56 | all traffic on the *host* port `25566` to the *container* port `25565`. 57 | 58 | `env` allows us to define environment variables that will be passed to the container. The `itzg/minecraft-server` image 59 | has [many environment variables](https://docker-minecraft-server.readthedocs.io/en/latest/variables/) that can be used 60 | to configure your MC server. In this case, we are setting up a Fabric server with the `fabricproxy-lite` mod. It is also 61 | set to offline mode as required by Velocity. 62 | 63 | `volumes` allows us to define which directories on the host we want to mount into the container. In this case, we are 64 | mounting the `/srv/smp` directory on the host to the `/data` directory in the container. This is where the Minecraft 65 | server data will be saved. The source directory can be any directory on the host, but it must be mounted to `/data` for 66 | this image. 67 | > [!IMPORTANT] 68 | > If you do not set up a volume mount for the `/data` directory, ***ALL YOUR SERVER DATA WILL BE LOST*** on restarts. 69 | 70 | ## Configuring Identity Forwarding 71 | 72 | Since Velocity is set up with "modern" forwarding and in online mode, we need a bit of configuration on our server 73 | before players will be able to connect. Luckily this is very simple! We just need to add a file to the `/srv/smp` 74 | directory that we mounted into the container. 75 | 76 | ### `/srv/smp/config/FabricProxy-Lite.toml` 77 | 78 | ```toml 79 | hackOnlineMode = true 80 | hackEarlySend = false 81 | hackMessageChain = true 82 | disconnectMessage = "This server requires you to connect with Velocity." 83 | secret = "" 84 | ``` 85 | 86 | You can find the `forwarding.secret` file next to your `velocity.toml`. 87 | 88 | ## Pre-pulling the Docker Image 89 | 90 | > [!TIP] 91 | > This is not strictly required, but highly recommended. It will speed up the server start time and connection times. 92 | > To pre-pull the docker image, simply run the following command on your backend server: 93 | 94 | ```bash 95 | docker pull itzg/minecraft-server:latest 96 | ``` 97 | -------------------------------------------------------------------------------- /docker-broker/src/main/kotlin/club/arson/impulse/dockerbroker/DockerServerConfig.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Impulse Server Manager for Velocity 3 | * Copyright (c) 2025 Dabb1e 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Affero General Public License as 7 | * published by the Free Software Foundation, either version 3 of the 8 | * License, or (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Affero General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Affero General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package club.arson.impulse.dockerbroker 20 | 21 | import club.arson.impulse.api.config.BrokerConfig 22 | import club.arson.impulse.dockerbroker.ImagePullPolicy.* 23 | import kotlinx.serialization.KSerializer 24 | import kotlinx.serialization.Serializable 25 | import kotlinx.serialization.descriptors.PrimitiveKind 26 | import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor 27 | import kotlinx.serialization.descriptors.SerialDescriptor 28 | import kotlinx.serialization.encoding.Decoder 29 | import kotlinx.serialization.encoding.Encoder 30 | 31 | /** 32 | * Configuration for a Docker server 33 | * 34 | * This configuration is used when the server type is "docker". 35 | * @property image Docker image to use for the server 36 | * @property portBindings List of port mappings (i.e "25566:25565") 37 | * @property hostPath Path to docker socket (accepts network sockets) 38 | * @property volumes Volumes to mount into the container ("/host/path:/container/path") 39 | * @property env Map of environment variables to set in the container 40 | */ 41 | @BrokerConfig("docker") 42 | @Serializable 43 | data class DockerServerConfig( 44 | var address: String? = null, 45 | var image: String = "itzg/minecraft-server", 46 | var imagePullPolicy: ImagePullPolicy = IF_NOT_PRESENT, 47 | var autoStartOnCreate: Boolean = false, 48 | var portBindings: List = listOf("25565:25565"), 49 | var hostPath: String = "unix:///var/run/docker.sock", 50 | var tlsConfig: DockerTlsConfig = DockerTlsConfig(), 51 | var volumes: List = emptyList(), 52 | var env: Map = mapOf("ONLINE_MODE" to "false"), 53 | ) 54 | 55 | /** 56 | * TLS configuration for the Docker connection 57 | * 58 | * @property enabled Whether TLS is enabled or not 59 | * @property tlsVerify Whether to verify the server's certificate 60 | * @property clientKeyPassword Password for the client key (if different from the keystore password) 61 | * @property keystorePath Path to the java PKCS12 keystore holding the client key, client cert, and CA cert 62 | * @property keystorePassword Password for the keystore 63 | */ 64 | @Serializable 65 | data class DockerTlsConfig( 66 | var enabled: Boolean = false, 67 | var tlsVerify: Boolean = true, 68 | var clientKeyPassword: String? = null, 69 | var keystorePath: String? = null, 70 | var keystorePassword: String? = null 71 | ) 72 | 73 | /** 74 | * Image pull policy for a Docker container images 75 | * 76 | * @property ALWAYS Always pull the image 77 | * @property IF_NOT_PRESENT Pull the image if it is not present 78 | * @property NEVER Never pull the image 79 | */ 80 | @Serializable(with = ImagePullPolicySerializer::class) 81 | enum class ImagePullPolicy(private val value: String) { 82 | ALWAYS("Always"), 83 | IF_NOT_PRESENT("IfNotPresent"), 84 | NEVER("Never"); 85 | 86 | override fun toString(): String { 87 | return value 88 | } 89 | } 90 | 91 | /** 92 | * Serializer for [ImagePullPolicy] 93 | */ 94 | object ImagePullPolicySerializer : KSerializer { 95 | override val descriptor: SerialDescriptor = 96 | PrimitiveSerialDescriptor("ImagePullPolicy", PrimitiveKind.STRING) 97 | 98 | override fun serialize(encoder: Encoder, value: ImagePullPolicy) { 99 | encoder.encodeString(value.toString()) 100 | } 101 | 102 | override fun deserialize(decoder: Decoder): ImagePullPolicy { 103 | val value = decoder.decodeString() 104 | return ImagePullPolicy.entries.first { it.toString() == value } 105 | } 106 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/club/arson/impulse/Impulse.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Impulse Server Manager for Velocity 3 | * Copyright (c) 2025 Dabb1e 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Affero General Public License as 7 | * published by the Free Software Foundation, either version 3 of the 8 | * License, or (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Affero General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Affero General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package club.arson.impulse 20 | 21 | import club.arson.impulse.api.events.RegisterBrokerEvent 22 | import club.arson.impulse.commands.createImpulseCommand 23 | import club.arson.impulse.config.ConfigManager 24 | import club.arson.impulse.inject.modules.BaseModule 25 | import club.arson.impulse.inject.modules.BrokerModule 26 | import club.arson.impulse.inject.modules.ConfigManagerModule 27 | import club.arson.impulse.server.ServerManager 28 | import com.google.inject.Guice 29 | import com.google.inject.Inject 30 | import com.velocitypowered.api.event.Subscribe 31 | import com.velocitypowered.api.event.proxy.ProxyInitializeEvent 32 | import com.velocitypowered.api.event.proxy.ProxyShutdownEvent 33 | import com.velocitypowered.api.plugin.Plugin 34 | import com.velocitypowered.api.plugin.annotation.DataDirectory 35 | import com.velocitypowered.api.proxy.ProxyServer 36 | import org.slf4j.Logger 37 | import java.io.File 38 | import java.nio.file.Path 39 | 40 | /** 41 | * Main Velocity plugin class for Impulse 42 | * 43 | * @property proxy a ref to the Velocity proxy server 44 | * @property logger a ref to the Velocity logger 45 | * @property dataDirectory the path to the data directory 46 | * @constructor creates a new Impulse plugin instance 47 | */ 48 | @Plugin( 49 | id = "impulse", 50 | name = "Impulse", 51 | version = BuildConstants.VERSION, 52 | authors = ["Dabb1e"], 53 | url = "https://github.com/Arson-Club/Impulse", 54 | description = "Dynamically start, stop, and create servers with Velocity" 55 | ) 56 | class Impulse @Inject constructor( 57 | private val proxy: ProxyServer, 58 | private val logger: Logger, 59 | @DataDirectory val dataDirectory: Path 60 | ) { 61 | private fun getLocalPlugins(): Set { 62 | val pluginDirectory = dataDirectory.toFile() 63 | return if (pluginDirectory.isDirectory) { 64 | pluginDirectory.listFiles { file -> file.extension == "jar" }?.toSet() ?: emptySet() 65 | } else { 66 | emptySet() 67 | } 68 | } 69 | 70 | private fun loadPlugins(plugins: Set) { 71 | plugins.forEach { plugin -> 72 | logger.info("Loading plugin $plugin") 73 | proxy.pluginManager.addToClasspath(this, plugin.toPath()) 74 | } 75 | } 76 | 77 | /** 78 | * Handles the setup needed for Impulse 79 | * 80 | * This method is called when Impulse is initialized by Velocity. It primarily sets up the service registry as well 81 | * as registering the custom commands and top level event listeners. 82 | * @param event the ProxyInitializeEvent fired by Velocity 83 | */ 84 | @Subscribe 85 | fun onProxyInitialization(event: ProxyInitializeEvent) { 86 | logger.info("Initializing Impulse") 87 | loadPlugins(getLocalPlugins()) 88 | 89 | ServiceRegistry.instance.injector = Guice.createInjector( 90 | BrokerModule(logger), BaseModule( 91 | this, proxy, dataDirectory, logger 92 | ), ConfigManagerModule(true) 93 | ) 94 | 95 | ServiceRegistry.instance.configManager = 96 | ServiceRegistry.instance.injector!!.getInstance(ConfigManager::class.java) 97 | ServiceRegistry.instance.serverManager = 98 | ServiceRegistry.instance.injector!!.getInstance(ServerManager::class.java) 99 | proxy.eventManager.register( 100 | this, 101 | ServiceRegistry.instance.injector!!.getInstance(PlayerLifecycleListener::class.java) 102 | ) 103 | 104 | // Register custom commands 105 | val commandManager = proxy.commandManager 106 | val impulseCommandMeta = commandManager.metaBuilder("impulse") 107 | .aliases("imp") 108 | .plugin(this) 109 | .build() 110 | commandManager.register(impulseCommandMeta, createImpulseCommand()) 111 | } 112 | 113 | /** 114 | * Handles the cleanup needed for Impulse 115 | * 116 | * @param event the ProxyShutdownEvent fired by Velocity 117 | */ 118 | @Subscribe 119 | fun onProxyShutdown(event: ProxyShutdownEvent) { 120 | logger.info("Shutting down Impulse") 121 | } 122 | 123 | /** 124 | * Handles when a broker is dynamically registered via the [RegisterBrokerEvent] 125 | * 126 | * @param event the RegisterBrokerEvent containing the jar ref 127 | */ 128 | @Subscribe 129 | fun onRegisterBrokerEvent(event: RegisterBrokerEvent) { 130 | if (event.result.isAllowed) { 131 | loadPlugins(setOf(event.jarPath.toFile())) 132 | } 133 | } 134 | } -------------------------------------------------------------------------------- /command-broker/src/main/kotlin/club/arson/impulse/commandbroker/CommandBroker.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Impulse Server Manager for Velocity 3 | * Copyright (c) 2025 Dabb1e 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Affero General Public License as 7 | * published by the Free Software Foundation, either version 3 of the 8 | * License, or (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Affero General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Affero General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package club.arson.impulse.commandbroker 20 | 21 | import club.arson.impulse.api.config.ServerConfig 22 | import club.arson.impulse.api.server.Broker 23 | import club.arson.impulse.api.server.Status 24 | import org.slf4j.Logger 25 | import java.io.File 26 | import java.net.InetSocketAddress 27 | 28 | /** 29 | * This broker is designed to run raw commands on the backend to create a server. The resulting PID is then managed as 30 | * if it were a valid server. 31 | * 32 | * @property serverConfig Server configuration to create a command broker for 33 | * @property logger Logger ref for log messages 34 | */ 35 | class CommandBroker(serverConfig: ServerConfig, private val logger: Logger? = null) : Broker { 36 | private var commandConfig: CommandBrokerConfig 37 | private var process: Process? = null 38 | 39 | init { 40 | commandConfig = serverConfig.config as CommandBrokerConfig 41 | } 42 | 43 | /** 44 | * Get the address of the server 45 | * 46 | * @return A result containing the address of the server if we were able to get it, else an error 47 | */ 48 | override fun address(): Result { 49 | if (commandConfig.address == null) { 50 | return Result.failure(IllegalArgumentException("No address specified in config")) 51 | } 52 | val port = commandConfig.address?.split(":")?.getOrNull(1)?.toIntOrNull() ?: 25565 53 | return runCatching { InetSocketAddress(commandConfig.address, port) } 54 | } 55 | 56 | /** 57 | * Check if the server is running 58 | * @return true if the server is running, else false 59 | */ 60 | override fun isRunning(): Boolean { 61 | return getStatus() == Status.RUNNING 62 | } 63 | 64 | /** 65 | * Get the status of the server 66 | * @return The status of the server 67 | */ 68 | override fun getStatus(): Status { 69 | return if (process?.isAlive == true) { 70 | Status.RUNNING 71 | } else { 72 | Status.STOPPED 73 | } 74 | } 75 | 76 | /** 77 | * Attempt to start the server if it is not already running 78 | * 79 | * @return success if the server was started, else an error 80 | */ 81 | override fun startServer(): Result { 82 | if (!isRunning()) { 83 | return runCatching { 84 | val commands = commandConfig.command 85 | logger?.debug("Starting server with command: ${commands.joinToString(" ")}") 86 | process = ProcessBuilder() 87 | .command(commands) 88 | .directory(File(commandConfig.workingDirectory)) 89 | .start() 90 | } 91 | } 92 | return Result.success(Unit) 93 | } 94 | 95 | /** 96 | * Attempt to stop the server if it is running 97 | * 98 | * @return success if the server was stopped, else an error 99 | */ 100 | override fun stopServer(): Result { 101 | if (isRunning()) { 102 | process?.destroy() 103 | } 104 | 105 | return Result.success(Unit) 106 | } 107 | 108 | /** 109 | * Attempt to remove the server 110 | * 111 | * Removing the server is the same as stopping it for the command broker since we can't reliably suspend the 112 | * subprocess cross-platform. 113 | * @return success if the server was removed, else an error 114 | */ 115 | override fun removeServer(): Result { 116 | // For the command broker there is no real difference between stopping and removing the server 117 | return stopServer() 118 | } 119 | 120 | /** 121 | * Reconcile any changes to our configuration. 122 | * 123 | * Since we are a generic broker, assume all changes require a restart. 124 | * @param config Server configuration to reconcile 125 | * @return the closure to actually do the reconciliation 126 | */ 127 | override fun reconcile(config: ServerConfig): Result { 128 | if (config.type != "cmd") { 129 | return Result.failure(IllegalArgumentException("Expected CommandBrokerConfig and got something else!")) 130 | } 131 | 132 | val newConfig = config.config as CommandBrokerConfig 133 | return if (newConfig != commandConfig) { 134 | Result.success(Runnable { 135 | stopServer() 136 | commandConfig = newConfig 137 | startServer() 138 | }) 139 | } else { 140 | Result.success(Runnable { 141 | commandConfig = newConfig 142 | }) 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /app/src/main/kotlin/club/arson/impulse/PlayerLifecycleListener.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Impulse Server Manager for Velocity 3 | * Copyright (c) 2025 Dabb1e 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Affero General Public License as 7 | * published by the Free Software Foundation, either version 3 of the 8 | * License, or (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Affero General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Affero General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package club.arson.impulse 20 | 21 | import com.velocitypowered.api.event.EventTask 22 | import com.velocitypowered.api.event.PostOrder 23 | import com.velocitypowered.api.event.Subscribe 24 | import com.velocitypowered.api.event.connection.DisconnectEvent 25 | import com.velocitypowered.api.event.player.ServerPreConnectEvent 26 | import com.velocitypowered.api.event.player.ServerPreConnectEvent.ServerResult 27 | import com.velocitypowered.api.proxy.Player 28 | import com.velocitypowered.api.proxy.server.RegisteredServer 29 | import net.kyori.adventure.text.Component 30 | import net.kyori.adventure.text.minimessage.MiniMessage 31 | import org.slf4j.Logger 32 | import javax.inject.Inject 33 | 34 | /** 35 | * Listens for player lifecycle events and processes them 36 | * 37 | * Listens for connect and disconnect events so that we can start and stop servers 38 | * @param logger the logger to write messages to 39 | * @constructor creates a new PlayerLifecycleListener registered with an optional logger. 40 | */ 41 | class PlayerLifecycleListener @Inject constructor(private val logger: Logger) { 42 | private fun getMM(message: String?): Component { 43 | return MiniMessage 44 | .miniMessage() 45 | .deserialize(message ?: "Unknown error") 46 | } 47 | 48 | /** 49 | * Either drops the player or sends a message depending on if they are transferring or not 50 | */ 51 | private fun handleTimeout( 52 | player: Player, 53 | previousServer: RegisteredServer?, 54 | message: String? = null 55 | ): ServerResult { 56 | if (previousServer == null) { 57 | // This is a workaround so that velocity will continue to hunt the try list 58 | throw IllegalStateException("Server hit timeout while starting: ${message ?: "Unknown error"}") 59 | } else { 60 | player.sendMessage(getMM(message)) 61 | } 62 | 63 | return ServerResult.denied() 64 | } 65 | 66 | fun handlePlayerConnectEvent(event: ServerPreConnectEvent) { 67 | val server = ServiceRegistry.instance.serverManager?.getServer(event.originalServer.serverInfo.name) 68 | if (server != null) { 69 | val prevServer = 70 | if (event.previousServer != null) ServiceRegistry.instance.serverManager?.getServer(event.previousServer!!.serverInfo.name) else null 71 | var isRunning = server.isRunning() 72 | 73 | // if the server is not running and auto start is enabled, start the server 74 | if (!isRunning && server.config.lifecycleSettings.allowAutoStart) { 75 | server.startServer().onSuccess { 76 | logger.debug("Server started successfully, allowing connection") 77 | isRunning = true 78 | }.onFailure { 79 | logger.warn("Error: failed to start server, rejecting connection") 80 | logger.warn(it.message) 81 | } 82 | } 83 | 84 | // If we are started, await ready and transfer the player 85 | if (isRunning) { 86 | server.awaitReady().onSuccess { 87 | logger.trace("Server reporting ready, transferring player") 88 | prevServer?.handleDisconnect(event.player.username) 89 | }.onFailure { 90 | logger.debug("Server failed to report ready, rejecting connection") 91 | event.result = handleTimeout( 92 | event.player, 93 | event.previousServer, 94 | ServiceRegistry.instance.configManager?.messages?.startupError 95 | ) 96 | } 97 | } else if (!server.config.lifecycleSettings.allowAutoStart) { 98 | // If we are not started and auto start is disabled, reject the connection with the correct message 99 | event.result = handleTimeout( 100 | event.player, 101 | event.previousServer, 102 | ServiceRegistry.instance.configManager?.messages?.autoStartDisabled 103 | ) 104 | } else { 105 | // Otherwise reject with an unknown error 106 | event.result = handleTimeout( 107 | event.player, 108 | event.previousServer, 109 | ServiceRegistry.instance.configManager?.messages?.startupError 110 | ) 111 | } 112 | } else { 113 | logger.debug("Server is not managed by us, taking no action") 114 | } 115 | } 116 | 117 | /** 118 | * Handles the ServerPreConnectEvent 119 | * 120 | * This event is fired when a player is about to connect to a server. We use this event to start the server if it is not already running. 121 | * @param event the ServerPreConnectEvent 122 | * @return an EventTask that will start the server if it is not already running 123 | * @see [ServerPreConnectEvent](https://jd.papermc.io/velocity/3.4.0/com/velocitypowered/api/event/player/ServerPreConnectEvent.html) 124 | */ 125 | @Subscribe(order = PostOrder.FIRST) 126 | fun onServerPreConnectEvent(event: ServerPreConnectEvent): EventTask { 127 | logger.debug("Handling ServerPreConnectEvent for ${event.player.username} from ${event.previousServer?.serverInfo?.name ?: "No Previous Server"} to ${event.originalServer.serverInfo.name}") 128 | return EventTask.async { 129 | handlePlayerConnectEvent(event) 130 | } 131 | } 132 | 133 | /** 134 | * Handles the DisconnectEvent 135 | * 136 | * This is fired when a player disconnects from the server. We will use this to schedule a shutdown if the server is empty. 137 | * @param event the DisconnectEvent 138 | * @see [DisconnectEvent](https://jd.papermc.io/velocity/3.4.0/com/velocitypowered/api/event/connection/DisconnectEvent.html) 139 | */ 140 | @Subscribe(order = PostOrder.LAST) 141 | fun onDisconnectEvent(event: DisconnectEvent) { 142 | runCatching { 143 | event.player.currentServer.get().server 144 | }.onSuccess { 145 | ServiceRegistry.instance.serverManager?.getServer(it.serverInfo.name) 146 | ?.handleDisconnect(event.player.username) 147 | }.onFailure { 148 | logger.debug("unable to determine tha disconnect server for ${event.player.username}") 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /docs/reference/docker-broker.md: -------------------------------------------------------------------------------- 1 | # Docker Broker 2 | 3 | The docker broker connects directly to a Docker daemon. It is a good choice for small to medium-sized deployments since 4 | it is relatively easy to set up while still having a good amount of advanced options for configuration. It is included 5 | in the default Impulse distribution. 6 | 7 | The docker broker is capable of connecting to a remote docker daemon. This is useful for advanced setups where you want 8 | to run the servers on a different machine. You can find instructions on how to set up a remote docker daemon 9 | [here](https://docs.docker.com/engine/daemon/remote-access/). 10 | > [!WARNING] 11 | > Connecting over TLS has experimental support. Verify its security before using it in a production environment. 12 | 13 | ## Configuration 14 | 15 | Docker broker specific configuration values. These should be nested under the `docker` key in the server configuration. 16 | 17 | | Key | Type | Description | Default | 18 | |---------------------|----------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------| 19 | | `image` | `string` | Docker image to use for the server | `itzg/minecraft-server` | 20 | | `address` | `string` | Used to specify the connection address when configured as a dynamic server. Format is `host:port` | `null` | 21 | | `imagePullPolicy` | `enum` | One of `Always`, `Never`, `IfNotPresent`. Controls under what circumstance we should attempt to pull the configured image automatically. See [Image Pull Policy](#Image-Pull-Policy) for details | `IfNotPresent` | 22 | | `autoStartOnCreate` | `boolean` | Controls autostart behaviour for the container. If set to true we will start the container at creation time, not when a user connects. | `false` | 23 | | `portBindings` | `list` | List of port bindings to use for the server. Each entry should be a string in the format `hostPort:containerPort` | `["25565:25565"]` | 24 | | `hostPath` | `string` | URI to the docker daemon location. This can either be a local socket, or a remote host. | `unix:///var/run/docker.sock` | 25 | | `tlsConfig` | `object` | TLS related configuration options if connecting to the docker daemon over TCP | | 26 | | `volumes` | `list` | List of host directories to mount into the container. These should be strings in the format `/host/path:/container/path` | `{}` | 27 | | `env` | `Map` | Map of environment variables to pass to the container. The key is the variable name, and the value is the variable value. | `{"ONLINE_MODE": "false"}` | 28 | 29 | ### TLS Configuration 30 | 31 | | Key | Type | Description | Default | 32 | |---------------------|-----------|----------------------------------------------------------------------------------------|---------| 33 | | `enabled` | `boolean` | Should we enable TLS for this docker connection | `false` | 34 | | `tlsVerify` | `boolean` | Should we verify the host certificate for this connection | `true` | 35 | | `clientKeyPassword` | `string` | Password for the client key if it differs from the `keystorePassowrd` | `null` | 36 | | `keystorePath` | `string` | Java PKCS12 formatted keystore that contains your client cert, client key, and CA cert | `null` | 37 | | `keystorePassword` | `string` | Password for accessing the configured keystore | `null` | 38 | 39 | ### Image Pull Policy 40 | 41 | The image pull policy controls when we should attempt to pull the configured image. The following policies are 42 | available: 43 | 44 | - `Always`: Always attempt to pull the image before starting the container 45 | - `Never`: Never attempt to pull the image before starting the container 46 | - `IfNotPresent`: Only attempt to pull the image if it is not already present on the host 47 | 48 | ## Reconciliation Behavior 49 | 50 | Most changes under the `docker` configuration key will require a server recreation to take effect. The docker broker 51 | will automatically stop, remove, and recreate the server container as needed. Because of this behavior, it is important 52 | to make sure any data you want to keep is stored in a volume. If not it will be lost during a reconciliation event. This 53 | can normally be accomplished by mounting a volume to the `/data` directory in the container. 54 | 55 | ## Using TLS 56 | 57 | Docker uses TLS to secure connections the the daemon over TCP. If you are using a remote docker daemon, it is highly 58 | encouraged to use TLS to secure the connection. The following instuctions will go over how to generate the keystore for 59 | Impulse from your PKI. To set up the certificates and keys in the first place 60 | follow [docker's guide](https://docs.docker.com/engine/security/protect-access/) 61 | 62 | ### Generating the Keystore 63 | 64 | First, If you need to convert your client credentials to PKCS12 format you can use the following command. Replace 65 | `` with a real password. 66 | 67 | ```shell 68 | openssl pkcs12 -export -in client.crt -inkey client.key \ 69 | -out client-keystore.p12 -name client-cert -passout pass: 70 | ```` 71 | 72 | Next generate the keystore by importing the client certificate and key. 73 | 74 | ```shell 75 | keytool -importkeystore -srckeystore docker-keystore.p12 -srcstoretype PKCS12 \ 76 | -srcstorepass -destkeystore docker-keystore.p12 -deststoretype PKCS12 \ 77 | -deststorepass 78 | ``` 79 | 80 | Lastly, inject the CA certificate into the keystore. 81 | 82 | ```shell 83 | keytool -importcert -alias ca-cert -file ca.crt \ 84 | -keystore docker-keystore.p12 -storepass your_keystore_password -noprompt 85 | ```` 86 | 87 | Your keystore should now be set up and ready to use with Impulse. 88 | 89 | ## Example Configuration 90 | 91 | ```yaml 92 | servers: 93 | - name: "lobby" 94 | type: "docker" 95 | docker: 96 | portBindings: 97 | - "25566:25565" 98 | volumes: 99 | - "/path/to/host/data:/data" 100 | env: 101 | ONLINE_MODE: "false" 102 | EULA: "true" 103 | ``` -------------------------------------------------------------------------------- /app/src/main/kotlin/club/arson/impulse/server/ServerManager.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Impulse Server Manager for Velocity 3 | * Copyright (c) 2025 Dabb1e 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Affero General Public License as 7 | * published by the Free Software Foundation, either version 3 of the 8 | * License, or (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Affero General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Affero General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package club.arson.impulse.server 20 | 21 | import club.arson.impulse.Impulse 22 | import club.arson.impulse.ServiceRegistry 23 | import club.arson.impulse.api.config.ServerConfig 24 | import club.arson.impulse.api.events.ConfigReloadEvent 25 | import club.arson.impulse.api.server.Broker 26 | import club.arson.impulse.inject.modules.ServerModule 27 | import com.google.inject.Inject 28 | import com.velocitypowered.api.event.EventTask 29 | import com.velocitypowered.api.event.Subscribe 30 | import com.velocitypowered.api.proxy.ProxyServer 31 | import com.velocitypowered.api.scheduler.ScheduledTask 32 | import org.slf4j.Logger 33 | import java.util.concurrent.TimeUnit 34 | 35 | class ServerManager @Inject constructor( 36 | private val proxy: ProxyServer, 37 | private val plugin: Impulse, 38 | private val logger: Logger 39 | ) { 40 | var servers = mutableMapOf() 41 | private var maintenanceInterval: Long 42 | private var maintenanceTask: ScheduledTask 43 | 44 | init { 45 | proxy.eventManager.register(plugin, this) 46 | val config = ServiceRegistry.instance.configManager 47 | reconcileServers(emptyList(), config?.servers ?: emptyList()) 48 | maintenanceInterval = config?.maintenanceInterval ?: 300 49 | maintenanceTask = proxy.scheduler 50 | .buildTask(plugin, this::serverMaintenance) 51 | .repeat(maintenanceInterval, TimeUnit.SECONDS) 52 | .schedule() 53 | } 54 | 55 | fun serverMaintenance() { 56 | servers.values.forEach { server -> 57 | if (server.serverRef.playersConnected.isEmpty() && server.config.lifecycleSettings.allowAutoStop && !server.pinned) { 58 | logger.trace("Found empty server ${server.serverRef.serverInfo.name}") 59 | server.scheduleShutdown() 60 | } 61 | } 62 | } 63 | 64 | fun getServer(name: String): Server? { 65 | return servers[name] 66 | } 67 | 68 | private fun createServer(serverName: String, config: ServerConfig): Result { 69 | val brokerFactory = ServiceRegistry.instance.getServerBroker() 70 | 71 | return runCatching { 72 | brokerFactory.createFromConfig(config, logger) 73 | .onSuccess { broker -> 74 | registerServer(broker, serverName, config) 75 | } 76 | .onFailure { exception -> 77 | logger.warn( 78 | "ServerManager: Unable to find valid broker for $serverName: ${exception.message}" 79 | ) 80 | } 81 | } 82 | } 83 | 84 | private fun registerServer(broker: Broker, serverName: String, config: ServerConfig) { 85 | ServiceRegistry.instance.injector?.createChildInjector(ServerModule(proxy, config, broker, logger)) 86 | ?.let { injector -> 87 | runCatching { 88 | servers[serverName] = injector.getInstance(Server::class.java) 89 | }.fold( 90 | onSuccess = { 91 | logger.debug("ServerManager: Created server $serverName") 92 | }, 93 | onFailure = { 94 | logger.warn("ServerManager: Failed to create server $serverName: ${it.message}") 95 | } 96 | ) 97 | } ?: run { 98 | logger.error("ServerManager: Failed to create child injector for server $serverName") 99 | return 100 | } 101 | } 102 | 103 | fun removeServer(serverName: String) { 104 | servers[serverName]?.removeServer()?.onFailure { error -> 105 | logger.error("Server $error failed to remove, it may be in an invalid state") 106 | } 107 | 108 | // Only remove the server record if it is dynamic 109 | val velocityServer = proxy.getServer(serverName).orElse(null) 110 | if (velocityServer != null && !proxy.configuration.servers.keys.contains(velocityServer.serverInfo.name)) { 111 | proxy.unregisterServer(velocityServer.serverInfo) 112 | } 113 | 114 | servers.remove(serverName) 115 | } 116 | 117 | private fun reconcileServers(oldConfigs: List, newConfigs: List) { 118 | val oldServerNames = servers.keys.toSet() 119 | val newServerNames = newConfigs.map { it.name } 120 | 121 | val toRemove = oldServerNames - newServerNames.toSet() 122 | toRemove.forEach(::removeServer) 123 | 124 | val toAdd = newServerNames - oldServerNames 125 | toAdd.forEach { serverName -> 126 | newConfigs.find { it.name == serverName }?.let { config -> 127 | createServer(serverName, config).onSuccess { 128 | logger.info("ServerManager: server $serverName added") 129 | }.onFailure { 130 | logger.error("ServerManager: server $serverName failed to add: ${it.message}") 131 | } 132 | } 133 | } 134 | 135 | val toReconcile = newServerNames intersect oldServerNames 136 | toReconcile.forEach { serverName -> 137 | val newConfig = newConfigs.find { it.name == serverName } 138 | if (oldConfigs.find { it.name == serverName } == newConfig) { 139 | logger.trace("ServerManager: server $serverName configuration unchanged") 140 | return@forEach 141 | } 142 | servers[serverName]?.reconcile(newConfig!!)?.onSuccess { 143 | logger.info("ServerManager: server $serverName reconciled") 144 | }?.onFailure { 145 | logger.error("ServerManager: server $serverName failed to reconcile: ${it.message}") 146 | } 147 | } 148 | } 149 | 150 | fun handleConfigReloadEvent(event: ConfigReloadEvent) { 151 | logger.debug("ServerManager: starting server configuration reload") 152 | if (!event.result.isAllowed) { 153 | logger.trace("ServerManager: configuration reload denied") 154 | return 155 | } 156 | reconcileServers(event.oldConfig.servers, event.config.servers) 157 | if (event.config.serverMaintenanceInterval != maintenanceInterval) { 158 | maintenanceInterval = event.config.serverMaintenanceInterval 159 | maintenanceTask.cancel() 160 | maintenanceTask = proxy.scheduler 161 | .buildTask(plugin, this::serverMaintenance) 162 | .repeat(maintenanceInterval, TimeUnit.SECONDS) 163 | .schedule() 164 | } 165 | logger.info("ServerManager: server configuration reload complete") 166 | } 167 | 168 | @Subscribe 169 | fun onConfigReloadEvent(event: ConfigReloadEvent): EventTask { 170 | return EventTask.async { 171 | handleConfigReloadEvent(event) 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Impulse 2 | 3 | Impulse is a plugin for the Minecraft server proxy [Velocity](https://papermc.io/software/velocity). It adds the ability 4 | to dynamically start and stop servers on demand as players join and leave. Why run a server that is only used for a few 5 | hours a day? 6 | 7 |

8 | 9 | 10 | 11 | 12 |

13 | 14 | ## Key Features 15 | 16 | Impulse has many features to make managing your servers easy! Some highlights include: 17 | 18 | * Dynamic server creation and destruction - only run servers when your players are connected 19 | * First and third party brokers - Cores to drive your servers no matter how they are deployed, via Docker, JAR files, 20 | or something else. We are actively working on expanding our selection but you can find the full list 21 | [here](https://arson-club.github.io/Impulse/reference/brokers.html) 22 | * Automatic hot reload of configuration - update how impulse runs a server automatically, without a reload command! 23 | * Unmanaged server support - plays nice with any static servers you have configured in Velocity 24 | * Custom events - broadcasts custom events that allow you to extend Impulse's functionality 25 | * Third party Broker support - Implement your own Broker or use someone else's to manage your servers 26 | * FOSS - Impulse is commited to being free and open source always 27 | 28 | ## Documentation 29 | 30 | For more detailed information on how to use Impulse, see our [documentation](https://arson-club.github.io/Impulse/). 31 | For API documentation, see our [KDocs](https://arson-club.github.io/Impulse/kdocs/index.html). 32 | 33 | ## Installation 34 | 35 | In short, download our latest release from one of our sources and place it in your Velocity plugins folder. For more 36 | detailed instructions see our [installation](https://arson-club.github.io/Impulse/getting_started/installation.html) 37 | guide. 38 | 39 | Sources: 40 | 41 | - [Modrinth](https://modrinth.com/plugin/impulse-server-manager) 42 | - [Hangar](https://hangar.papermc.io/ArsonClub/Impulse) 43 | - [GitHub Releases](https://github.com/Arson-Club/Impulse/releases) 44 | 45 | ## Quick Start 46 | 47 | > [!TIP] 48 | > Looking for a more in-depth guide? See 49 | > our [Getting Started](https://arson-club.github.io/Impulse/getting_started/index.html) 50 | > documentation. 51 | 52 | The following configuration should get you started with a simple lobby server. 53 | 54 | ### Step 1: Configure Velocity 55 | 56 | Simply add the server to your velocity config as normal. 57 | 58 | ```toml 59 | player-info-forwarding = "modern" 60 | 61 | [servers] 62 | lobby = "127.0.0.1:25566" 63 | 64 | try = ["lobby"] 65 | 66 | ``` 67 | 68 | ### Step 2: Configure Impulse 69 | 70 | Configure impulse so it know how to manage your server 71 | 72 | ```yaml 73 | instanceName: Bones 74 | servers: 75 | - name: lobby 76 | inactiveTimeout: 300 77 | type: docker 78 | docker: 79 | image: itzg/minecraft-server 80 | portBindings: 81 | - "25566:25565" 82 | env: 83 | ONLINE_MODE: "FALSE" 84 | TYPE: "FABRIC" 85 | EULA: "TRUE" 86 | MODRINTH_PROJECTS: "fabricproxy-lite" 87 | DIFFICULTY: "PEACEFUL" 88 | ALLOW_NETHER: "FALSE" 89 | MODE: "adventure" 90 | volumes: 91 | - "/srv/lobby:/data" 92 | ``` 93 | 94 | ### Step 3: Configure the MC Server 95 | 96 | Add some config to allow for modern forwarding 97 | 98 | ```toml 99 | # create the file /srv/lobby/config/FabricProxy-Lite.toml 100 | hackOnlineMode = true 101 | hackEarlySend = false 102 | hackMessageChain = true 103 | disconnectMessage = "This server requires you to connect through the proxy." 104 | secret = "" 105 | ``` 106 | 107 | ### Step 4: Connect 108 | 109 | Simply start your velocity proxy and connect to it from your Minecraft client. If you run into issues check 110 | our [documentation](https://arson-club.github.io/Impulse/) or open an issue! 111 | 112 | ## Getting Help 113 | 114 | If your having problems with Impulse, experiencing a bug, or just want to recommend a feature, feel free 115 | to [open an issue](https://github.com/Arson-Club/Impulse/issues/new?template=Blank+issue)! I will do my best to 116 | respond. 117 | 118 | ## Contributing 119 | 120 | All contributions are welcome! For more specific instructions see 121 | our [contributing](https://arson-club.github.io/Impulse/contributing/contributing.html) page. 122 | 123 | For specifics on adding creating your own broker to integrate with another server platform 124 | see [our guide](https://arson-club.github.io/Impulse/contributing/creating-a-broker.html). 125 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s 90 | ' "$PWD" ) || exit 91 | 92 | # Use the maximum available, or set MAX_FD != -1 to use that value. 93 | MAX_FD=maximum 94 | 95 | warn () { 96 | echo "$*" 97 | } >&2 98 | 99 | die () { 100 | echo 101 | echo "$*" 102 | echo 103 | exit 1 104 | } >&2 105 | 106 | # OS specific support (must be 'true' or 'false'). 107 | cygwin=false 108 | msys=false 109 | darwin=false 110 | nonstop=false 111 | case "$( uname )" in #( 112 | CYGWIN* ) cygwin=true ;; #( 113 | Darwin* ) darwin=true ;; #( 114 | MSYS* | MINGW* ) msys=true ;; #( 115 | NONSTOP* ) nonstop=true ;; 116 | esac 117 | 118 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 119 | 120 | 121 | # Determine the Java command to use to start the JVM. 122 | if [ -n "$JAVA_HOME" ] ; then 123 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 124 | # IBM's JDK on AIX uses strange locations for the executables 125 | JAVACMD=$JAVA_HOME/jre/sh/java 126 | else 127 | JAVACMD=$JAVA_HOME/bin/java 128 | fi 129 | if [ ! -x "$JAVACMD" ] ; then 130 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 131 | 132 | Please set the JAVA_HOME variable in your environment to match the 133 | location of your Java installation." 134 | fi 135 | else 136 | JAVACMD=java 137 | if ! command -v java >/dev/null 2>&1 138 | then 139 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 140 | 141 | Please set the JAVA_HOME variable in your environment to match the 142 | location of your Java installation." 143 | fi 144 | fi 145 | 146 | # Increase the maximum file descriptors if we can. 147 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 148 | case $MAX_FD in #( 149 | max*) 150 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 151 | # shellcheck disable=SC2039,SC3045 152 | MAX_FD=$( ulimit -H -n ) || 153 | warn "Could not query maximum file descriptor limit" 154 | esac 155 | case $MAX_FD in #( 156 | '' | soft) :;; #( 157 | *) 158 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 159 | # shellcheck disable=SC2039,SC3045 160 | ulimit -n "$MAX_FD" || 161 | warn "Could not set maximum file descriptor limit to $MAX_FD" 162 | esac 163 | fi 164 | 165 | # Collect all arguments for the java command, stacking in reverse order: 166 | # * args from the command line 167 | # * the main class name 168 | # * -classpath 169 | # * -D...appname settings 170 | # * --module-path (only if needed) 171 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 172 | 173 | # For Cygwin or MSYS, switch paths to Windows format before running java 174 | if "$cygwin" || "$msys" ; then 175 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 176 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 177 | 178 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 179 | 180 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 181 | for arg do 182 | if 183 | case $arg in #( 184 | -*) false ;; # don't mess with options #( 185 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 186 | [ -e "$t" ] ;; #( 187 | *) false ;; 188 | esac 189 | then 190 | arg=$( cygpath --path --ignore --mixed "$arg" ) 191 | fi 192 | # Roll the args list around exactly as many times as the number of 193 | # args, so each arg winds up back in the position where it started, but 194 | # possibly modified. 195 | # 196 | # NB: a `for` loop captures its iteration list before it begins, so 197 | # changing the positional parameters here affects neither the number of 198 | # iterations, nor the values presented in `arg`. 199 | shift # remove old arg 200 | set -- "$@" "$arg" # push replacement arg 201 | done 202 | fi 203 | 204 | 205 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 206 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 207 | 208 | # Collect all arguments for the java command: 209 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 210 | # and any embedded shellness will be escaped. 211 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 212 | # treated as '${Hostname}' itself on the command line. 213 | 214 | set -- \ 215 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 216 | -classpath "$CLASSPATH" \ 217 | org.gradle.wrapper.GradleWrapperMain \ 218 | "$@" 219 | 220 | # Stop when "xargs" is not available. 221 | if ! command -v xargs >/dev/null 2>&1 222 | then 223 | die "xargs is not available" 224 | fi 225 | 226 | # Use "xargs" to parse quoted args. 227 | # 228 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 229 | # 230 | # In Bash we could simply go: 231 | # 232 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 233 | # set -- "${ARGS[@]}" "$@" 234 | # 235 | # but POSIX shell has neither arrays nor command substitution, so instead we 236 | # post-process each arg (as a line of input to sed) to backslash-escape any 237 | # character that might be a shell metacharacter, then use eval to reverse 238 | # that process (while maintaining the separation between arguments), and wrap 239 | # the whole thing up as a single "set" statement. 240 | # 241 | # This will of course break if any of these variables contains a newline or 242 | # an unmatched quote. 243 | # 244 | 245 | eval "set -- $( 246 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 247 | xargs -n1 | 248 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 249 | tr '\n' ' ' 250 | )" '"$@"' 251 | 252 | exec "$JAVACMD" "$@" 253 | --------------------------------------------------------------------------------