diff --git a/CHANGELOG.md b/CHANGELOG.md
index 477f90ef..a272678f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added TextColorArgument to minecraft-extras
- Added LocationArgument to cloud-bukkit
- Added ServerArgument to cloud-velocity
+ - Added LockableCommandManager to cloud-core
## [1.0.2] - 2020-10-18
diff --git a/cloud-core/src/main/java/cloud/commandframework/LockableCommandManager.java b/cloud-core/src/main/java/cloud/commandframework/LockableCommandManager.java
new file mode 100644
index 00000000..9f2b170f
--- /dev/null
+++ b/cloud-core/src/main/java/cloud/commandframework/LockableCommandManager.java
@@ -0,0 +1,123 @@
+//
+// MIT License
+//
+// Copyright (c) 2020 Alexander Söderberg & Contributors
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+package cloud.commandframework;
+
+import cloud.commandframework.execution.AsynchronousCommandExecutionCoordinator;
+import cloud.commandframework.execution.CommandExecutionCoordinator;
+import cloud.commandframework.internal.CommandRegistrationHandler;
+import org.checkerframework.checker.nullness.qual.NonNull;
+
+import java.util.function.Function;
+
+/**
+ * {@link CommandManager} implementation that allows you to lock command registrations.
+ * This should be used when the platform limits command registration to a certain point in time.
+ *
+ * To lock writes, use {@link #lockWrites()}. To check if writing is allowed, use {@link #isCommandRegistrationAllowed()}.
+ * If {@link #isCommandRegistrationAllowed()} is {@code false} then {@link #command(Command)} will throw
+ * {@link IllegalStateException}.
+ *
+ * @param Command sender type
+ * @since 1.1.0
+ */
+public abstract class LockableCommandManager extends CommandManager {
+
+ private final Object writeLock = new Object();
+ private volatile boolean writeLocked = false;
+
+ /**
+ * Create a new command manager instance
+ *
+ * @param commandExecutionCoordinator Execution coordinator instance. The coordinator is in charge of executing incoming
+ * commands. Some considerations must be made when picking a suitable execution coordinator
+ * for your platform. For example, an entirely asynchronous coordinator is not suitable
+ * when the parsers used in that particular platform are not thread safe. If you have
+ * commands that perform blocking operations, however, it might not be a good idea to
+ * use a synchronous execution coordinator. In most cases you will want to pick between
+ * {@link CommandExecutionCoordinator#simpleCoordinator()} and
+ * {@link AsynchronousCommandExecutionCoordinator}
+ * @param commandRegistrationHandler Command registration handler. This will get called every time a new command is
+ * registered to the command manager. This may be used to forward command registration
+ */
+ public LockableCommandManager(
+ final @NonNull Function<@NonNull CommandTree, @NonNull CommandExecutionCoordinator> commandExecutionCoordinator,
+ final @NonNull CommandRegistrationHandler commandRegistrationHandler
+ ) {
+ super(commandExecutionCoordinator, commandRegistrationHandler);
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * This should only be called when {@link #isCommandRegistrationAllowed()} is {@code false},
+ * else {@link IllegalStateException} will be called
+ *
+ * @param command Command to register
+ * @return
+ */
+ @Override
+ public final @NonNull CommandManager command(final @NonNull Command command) {
+ synchronized (this.writeLock) {
+ if (!isCommandRegistrationAllowed()) {
+ throw new IllegalStateException(
+ "Command registration is not allowed. The command manager has been locked."
+ );
+ }
+ return super.command(command);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * This should only be called when {@link #isCommandRegistrationAllowed()} is {@code false},
+ * else {@link IllegalStateException} will be called
+ *
+ * @param command Command to register. {@link Command.Builder#build()}} will be invoked.
+ * @return
+ */
+ @Override
+ public final @NonNull CommandManager command(final Command.@NonNull Builder command) {
+ return super.command(command);
+ }
+
+ /**
+ * Lock writing. After this, {@link #isCommandRegistrationAllowed()} will return {@code false}
+ */
+ protected final void lockWrites() {
+ synchronized (this.writeLock) {
+ this.writeLocked = true;
+ }
+ }
+
+ /**
+ * Check if command registration is allowed
+ *
+ * @return {@code true} if the registration is allowed, else {@code false}
+ */
+ public final boolean isCommandRegistrationAllowed() {
+ return !this.writeLocked;
+ }
+
+}
diff --git a/cloud-core/src/test/java/cloud/commandframework/LockableCommandManagerTest.java b/cloud-core/src/test/java/cloud/commandframework/LockableCommandManagerTest.java
new file mode 100644
index 00000000..ad9496ce
--- /dev/null
+++ b/cloud-core/src/test/java/cloud/commandframework/LockableCommandManagerTest.java
@@ -0,0 +1,45 @@
+//
+// MIT License
+//
+// Copyright (c) 2020 Alexander Söderberg & Contributors
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+package cloud.commandframework;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class LockableCommandManagerTest {
+
+ @Test
+ void testLockableCommandManager() {
+ final LockableCommandManager manager = new TestLockableCommandManager();
+ /* Add a command before locking */
+ manager.command(manager.commandBuilder("test1"));
+ /* Lock */
+ manager.lockWrites();
+ /* Add a command after locking */
+ Assertions.assertThrows(
+ IllegalStateException.class,
+ () -> manager.command(manager.commandBuilder("test2"))
+ );
+ }
+
+}
diff --git a/cloud-core/src/test/java/cloud/commandframework/TestLockableCommandManager.java b/cloud-core/src/test/java/cloud/commandframework/TestLockableCommandManager.java
new file mode 100644
index 00000000..6a295230
--- /dev/null
+++ b/cloud-core/src/test/java/cloud/commandframework/TestLockableCommandManager.java
@@ -0,0 +1,54 @@
+//
+// MIT License
+//
+// Copyright (c) 2020 Alexander Söderberg & Contributors
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+package cloud.commandframework;
+
+import cloud.commandframework.execution.CommandExecutionCoordinator;
+import cloud.commandframework.internal.CommandRegistrationHandler;
+import cloud.commandframework.meta.SimpleCommandMeta;
+
+public class TestLockableCommandManager extends LockableCommandManager {
+
+ /**
+ * Construct a new test command manager
+ */
+ public TestLockableCommandManager() {
+ super(
+ CommandExecutionCoordinator.simpleCoordinator(),
+ CommandRegistrationHandler.nullCommandRegistrationHandler()
+ );
+ }
+
+ @Override
+ public final SimpleCommandMeta createDefaultCommandMeta() {
+ return SimpleCommandMeta.empty();
+ }
+
+ @Override
+ public final boolean hasPermission(final TestCommandSender sender, final String permission) {
+ System.out.printf("Testing permission: %s\n", permission);
+ return !permission.equalsIgnoreCase("no");
+ }
+
+}
+