diff --git a/distributor-command-cloud/src/main/java/com/xpdustry/distributor/command/cloud/ArcCommandManager.java b/distributor-command-cloud/src/main/java/com/xpdustry/distributor/command/cloud/ArcCommandManager.java
index 323b5bcd..9eddcdc9 100644
--- a/distributor-command-cloud/src/main/java/com/xpdustry/distributor/command/cloud/ArcCommandManager.java
+++ b/distributor-command-cloud/src/main/java/com/xpdustry/distributor/command/cloud/ArcCommandManager.java
@@ -22,7 +22,9 @@
import com.xpdustry.distributor.command.cloud.parser.PlayerParser;
import com.xpdustry.distributor.command.cloud.parser.TeamParser;
import com.xpdustry.distributor.command.cloud.specifier.AllTeams;
+import com.xpdustry.distributor.core.DistributorProvider;
import com.xpdustry.distributor.core.command.CommandSender;
+import com.xpdustry.distributor.core.permission.PermissionManager;
import com.xpdustry.distributor.core.plugin.MindustryPlugin;
import com.xpdustry.distributor.core.plugin.PluginAware;
import io.leangen.geantyref.TypeToken;
@@ -105,7 +107,16 @@ public ArcCommandManager(
@Override
public boolean hasPermission(final @NonNull C sender, final String permission) {
- return permission.isEmpty() || senderMapper().reverse(sender).isServer(); // TODO Add permission
+ if (permission.isEmpty()) {
+ return true;
+ }
+ final var reversed = senderMapper().reverse(sender);
+ return reversed.isServer()
+ || DistributorProvider.get()
+ .getService(PermissionManager.class)
+ .orElseThrow()
+ .getPermission(reversed.getPlayer(), permission)
+ .asBoolean();
}
@Override
diff --git a/distributor-common/src/main/java/com/xpdustry/distributor/core/permission/ImmutablePermissionTree.java b/distributor-common/src/main/java/com/xpdustry/distributor/core/permission/ImmutablePermissionTree.java
new file mode 100644
index 00000000..6f59033d
--- /dev/null
+++ b/distributor-common/src/main/java/com/xpdustry/distributor/core/permission/ImmutablePermissionTree.java
@@ -0,0 +1,45 @@
+/*
+ * Distributor, a feature-rich framework for Mindustry plugins.
+ *
+ * Copyright (C) 2024 Xpdustry
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package com.xpdustry.distributor.core.permission;
+
+import java.util.Map;
+
+final class ImmutablePermissionTree implements PermissionTree {
+
+ private final PermissionTree inner;
+
+ ImmutablePermissionTree(final PermissionTree inner) {
+ this.inner = inner;
+ }
+
+ @Override
+ public TriState getPermission(final String permission) {
+ return inner.getPermission(permission);
+ }
+
+ @Override
+ public void setPermission(final String permission, final TriState state) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Map getPermissions() {
+ return inner.getPermissions();
+ }
+}
diff --git a/distributor-common/src/main/java/com/xpdustry/distributor/core/permission/PermissionManager.java b/distributor-common/src/main/java/com/xpdustry/distributor/core/permission/PermissionManager.java
new file mode 100644
index 00000000..c93ee03f
--- /dev/null
+++ b/distributor-common/src/main/java/com/xpdustry/distributor/core/permission/PermissionManager.java
@@ -0,0 +1,47 @@
+/*
+ * Distributor, a feature-rich framework for Mindustry plugins.
+ *
+ * Copyright (C) 2024 Xpdustry
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package com.xpdustry.distributor.core.permission;
+
+import java.util.regex.Pattern;
+import mindustry.gen.Player;
+
+public interface PermissionManager {
+
+ /**
+ * Regex pattern used to validate permission strings.
+ *
+ * Notes:
+ *
+ * Permission strings are composed of a series of nodes separated by dots with alphanumeric characters and minus
+ * signs, such as {@code "plugin.command"}.
+ *
+ * A parent node is always overridden by a child node, such as {@code "plugin.command"} overriding {@code "plugin"}.
+ *
+ * Wildcards are also allowed, but they currently have the same effect as a normal node, such as {@code "plugin.command.*"} equals {@code "plugin.command"}.
+ *
+ * The only relevant use of wildcards is the root permission {@code "*"} permission. It
+ * allows you to set a default value for all permissions.
+ *
+ */
+ String PERMISSION_REGEX = "^(\\*|[a-z\\d\\-]+)(\\.(\\*|[a-z\\d\\-]+))*$";
+
+ Pattern PERMISSION_PATTERN = Pattern.compile(PERMISSION_REGEX);
+
+ TriState getPermission(final Player player, final String permission);
+}
diff --git a/distributor-common/src/main/java/com/xpdustry/distributor/core/permission/PermissionTree.java b/distributor-common/src/main/java/com/xpdustry/distributor/core/permission/PermissionTree.java
new file mode 100644
index 00000000..ac397ed7
--- /dev/null
+++ b/distributor-common/src/main/java/com/xpdustry/distributor/core/permission/PermissionTree.java
@@ -0,0 +1,38 @@
+/*
+ * Distributor, a feature-rich framework for Mindustry plugins.
+ *
+ * Copyright (C) 2024 Xpdustry
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package com.xpdustry.distributor.core.permission;
+
+import java.util.Map;
+
+public interface PermissionTree {
+
+ static PermissionTree simple() {
+ return new SimplePermissionTree();
+ }
+
+ static PermissionTree immutable(final PermissionTree tree) {
+ return (tree instanceof ImmutablePermissionTree) ? tree : new ImmutablePermissionTree(tree);
+ }
+
+ TriState getPermission(final String permission);
+
+ void setPermission(final String permission, final TriState state);
+
+ Map getPermissions();
+}
diff --git a/distributor-common/src/main/java/com/xpdustry/distributor/core/permission/SimplePermissionTree.java b/distributor-common/src/main/java/com/xpdustry/distributor/core/permission/SimplePermissionTree.java
new file mode 100644
index 00000000..5da8f915
--- /dev/null
+++ b/distributor-common/src/main/java/com/xpdustry/distributor/core/permission/SimplePermissionTree.java
@@ -0,0 +1,121 @@
+/*
+ * Distributor, a feature-rich framework for Mindustry plugins.
+ *
+ * Copyright (C) 2024 Xpdustry
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package com.xpdustry.distributor.core.permission;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import org.jspecify.annotations.Nullable;
+
+final class SimplePermissionTree implements PermissionTree {
+
+ private final @Nullable SimplePermissionTree parent;
+ private final Map children = new HashMap<>();
+ private TriState value = TriState.UNDEFINED;
+
+ SimplePermissionTree() {
+ this.parent = null;
+ }
+
+ private SimplePermissionTree(final @Nullable SimplePermissionTree parent) {
+ this.parent = parent;
+ }
+
+ public TriState getPermission(final String permission) {
+ if (!PermissionManager.PERMISSION_PATTERN.matcher(permission).matches()) {
+ throw new IllegalArgumentException("The permission doesn't match the regex: " + permission);
+ }
+ var state = TriState.UNDEFINED;
+ var node = this;
+ for (final var part : permission.split("\\.", -1)) {
+ if (node.children.containsKey("*") && node.children.get("*").value != TriState.UNDEFINED) {
+ state = node.children.get("*").value;
+ }
+ node = node.children.get(part);
+ if (node == null) {
+ return state;
+ } else if (node.value != TriState.UNDEFINED) {
+ state = node.value;
+ }
+ }
+ return state;
+ }
+
+ public void setPermission(final String permission, final TriState state) {
+ if (!PermissionManager.PERMISSION_PATTERN.matcher(permission).matches()) {
+ throw new IllegalArgumentException("The permission doesn't match the regex: " + permission);
+ }
+ final var parts = permission.split("\\.", -1);
+ var node = this;
+ if (state != TriState.UNDEFINED) {
+ for (final var part : parts) {
+ final var parent = node;
+ node = node.children.computeIfAbsent(part, k -> new SimplePermissionTree(parent));
+ }
+ node.value = state;
+ } else {
+ for (final var part : parts) {
+ node = node.children.get(part);
+ if (node == null) {
+ return;
+ }
+ }
+ node.value = state;
+ var index = parts.length - 1;
+ while (node.parent != null && node.children.isEmpty()) {
+ node = node.parent;
+ node.children.remove(parts[index--]);
+ }
+ }
+ }
+
+ public Map getPermissions() {
+ final Map permissions = new HashMap<>();
+ for (final var child : this.children.entrySet()) {
+ if (child.getValue().value != TriState.UNDEFINED) {
+ permissions.put(child.getKey(), child.getValue().value.asBoolean());
+ }
+ for (final var entry : child.getValue().getPermissions().entrySet()) {
+ permissions.put(child.getKey() + "." + entry.getKey(), entry.getValue());
+ }
+ }
+ return Collections.unmodifiableMap(permissions);
+ }
+
+ @Override
+ public boolean equals(final @Nullable Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof final SimplePermissionTree that)) {
+ return false;
+ }
+ if (!this.children.equals(that.children)) {
+ return false;
+ }
+ return this.value == that.value;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = this.children.hashCode();
+ result = 31 * result + this.value.hashCode();
+ return result;
+ }
+}
diff --git a/distributor-common/src/main/java/com/xpdustry/distributor/core/permission/TriState.java b/distributor-common/src/main/java/com/xpdustry/distributor/core/permission/TriState.java
new file mode 100644
index 00000000..6e0c1de0
--- /dev/null
+++ b/distributor-common/src/main/java/com/xpdustry/distributor/core/permission/TriState.java
@@ -0,0 +1,44 @@
+/*
+ * Distributor, a feature-rich framework for Mindustry plugins.
+ *
+ * Copyright (C) 2024 Xpdustry
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package com.xpdustry.distributor.core.permission;
+
+import org.jspecify.annotations.Nullable;
+
+/**
+ * A ternary boolean type, used by the permission system.
+ */
+public enum TriState {
+ FALSE(false),
+ TRUE(true),
+ UNDEFINED(false);
+
+ private final boolean value;
+
+ TriState(final boolean value) {
+ this.value = value;
+ }
+
+ public static TriState of(final @Nullable Boolean state) {
+ return state == null ? UNDEFINED : state ? TRUE : FALSE;
+ }
+
+ public boolean asBoolean() {
+ return this.value;
+ }
+}
diff --git a/distributor-permission-rank/build.gradle.kts b/distributor-permission-rank/build.gradle.kts
new file mode 100644
index 00000000..e3f9ce9d
--- /dev/null
+++ b/distributor-permission-rank/build.gradle.kts
@@ -0,0 +1,15 @@
+plugins {
+ id("distributor4.base-conventions")
+ id("distributor4.mindustry-conventions")
+}
+
+module {
+ identifier = "distributor-permission"
+ display = "DistributorLoggerSimple"
+ main = "com.xpdustry.distributor.logger.simple.DistributorLoggerPlugin"
+ description = "Simple permission system based on linear ranks."
+}
+
+dependencies {
+ compileOnly(project(":distributor-common"))
+}
diff --git a/distributor-permission-rank/src/main/java/com/xpdustry/distributor/permission/LinearEnumRankNode.java b/distributor-permission-rank/src/main/java/com/xpdustry/distributor/permission/LinearEnumRankNode.java
new file mode 100644
index 00000000..5a1ad78a
--- /dev/null
+++ b/distributor-permission-rank/src/main/java/com/xpdustry/distributor/permission/LinearEnumRankNode.java
@@ -0,0 +1,46 @@
+/*
+ * Distributor, a feature-rich framework for Mindustry plugins.
+ *
+ * Copyright (C) 2024 Xpdustry
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package com.xpdustry.distributor.permission;
+
+import java.util.function.Function;
+import org.jspecify.annotations.Nullable;
+
+record LinearEnumRankNode>(E value, Function nameProvider, boolean ascending)
+ implements RankNode {
+
+ @Override
+ public String getName() {
+ return nameProvider.apply(value);
+ }
+
+ @Override
+ public @Nullable RankNode getPrevious() {
+ @SuppressWarnings("unchecked")
+ final var constants = (E[]) value.getClass().getEnumConstants();
+ if (ascending) {
+ return (value.ordinal() > 0)
+ ? new LinearEnumRankNode<>(constants[value.ordinal() - 1], nameProvider, true)
+ : null;
+ } else {
+ return (value.ordinal() + 1 < constants.length)
+ ? new LinearEnumRankNode<>(constants[value.ordinal() + 1], nameProvider, false)
+ : null;
+ }
+ }
+}
diff --git a/distributor-permission-rank/src/main/java/com/xpdustry/distributor/permission/RankNode.java b/distributor-permission-rank/src/main/java/com/xpdustry/distributor/permission/RankNode.java
new file mode 100644
index 00000000..98ef4f62
--- /dev/null
+++ b/distributor-permission-rank/src/main/java/com/xpdustry/distributor/permission/RankNode.java
@@ -0,0 +1,34 @@
+/*
+ * Distributor, a feature-rich framework for Mindustry plugins.
+ *
+ * Copyright (C) 2024 Xpdustry
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package com.xpdustry.distributor.permission;
+
+import java.util.function.Function;
+import org.jspecify.annotations.Nullable;
+
+public interface RankNode {
+
+ static > RankNode linearEnum(
+ final E value, final Function nameProvider, boolean ascending) {
+ return new LinearEnumRankNode<>(value, nameProvider, ascending);
+ }
+
+ String getName();
+
+ @Nullable RankNode getPrevious();
+}
diff --git a/distributor-permission-rank/src/main/java/com/xpdustry/distributor/permission/RankPermissionStorage.java b/distributor-permission-rank/src/main/java/com/xpdustry/distributor/permission/RankPermissionStorage.java
new file mode 100644
index 00000000..332b642e
--- /dev/null
+++ b/distributor-permission-rank/src/main/java/com/xpdustry/distributor/permission/RankPermissionStorage.java
@@ -0,0 +1,26 @@
+/*
+ * Distributor, a feature-rich framework for Mindustry plugins.
+ *
+ * Copyright (C) 2024 Xpdustry
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package com.xpdustry.distributor.permission;
+
+import com.xpdustry.distributor.core.permission.PermissionTree;
+
+public interface RankPermissionStorage {
+
+ PermissionTree getRankPermissions(final RankNode node);
+}
diff --git a/distributor-common/src/main/java/com/xpdustry/distributor/core/permission/Permissible.java b/distributor-permission-rank/src/main/java/com/xpdustry/distributor/permission/RankProvider.java
similarity index 79%
rename from distributor-common/src/main/java/com/xpdustry/distributor/core/permission/Permissible.java
rename to distributor-permission-rank/src/main/java/com/xpdustry/distributor/permission/RankProvider.java
index ba37c29b..ea3d2c72 100644
--- a/distributor-common/src/main/java/com/xpdustry/distributor/core/permission/Permissible.java
+++ b/distributor-permission-rank/src/main/java/com/xpdustry/distributor/permission/RankProvider.java
@@ -16,6 +16,12 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-package com.xpdustry.distributor.core.permission;
+package com.xpdustry.distributor.permission;
-public interface Permissible {}
+import java.util.Collection;
+import mindustry.gen.Player;
+
+public interface RankProvider {
+
+ Collection getRanks(final Player player);
+}
diff --git a/distributor-permission-rank/src/main/java/com/xpdustry/distributor/permission/SimpleRankPermissionManager.java b/distributor-permission-rank/src/main/java/com/xpdustry/distributor/permission/SimpleRankPermissionManager.java
new file mode 100644
index 00000000..774622a4
--- /dev/null
+++ b/distributor-permission-rank/src/main/java/com/xpdustry/distributor/permission/SimpleRankPermissionManager.java
@@ -0,0 +1,49 @@
+/*
+ * Distributor, a feature-rich framework for Mindustry plugins.
+ *
+ * Copyright (C) 2024 Xpdustry
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package com.xpdustry.distributor.permission;
+
+import com.xpdustry.distributor.core.permission.PermissionManager;
+import com.xpdustry.distributor.core.permission.TriState;
+import mindustry.gen.Player;
+
+final class SimpleRankPermissionManager implements PermissionManager {
+
+ private RankProvider provider;
+ private RankPermissionStorage storage;
+
+ SimpleRankPermissionManager(final RankProvider provider, final RankPermissionStorage storage) {
+ this.provider = provider;
+ this.storage = storage;
+ }
+
+ @Override
+ public TriState getPermission(final Player player, final String permission) {
+ for (final var node : this.provider.getRanks(player)) {
+ RankNode current = node;
+ while (current != null) {
+ final var state = this.storage.getRankPermissions(current).getPermission(permission);
+ if (state != TriState.UNDEFINED) {
+ return state;
+ }
+ current = node.getPrevious();
+ }
+ }
+ return TriState.UNDEFINED;
+ }
+}
diff --git a/distributor-permission-rank/src/main/java/com/xpdustry/distributor/permission/package-info.java b/distributor-permission-rank/src/main/java/com/xpdustry/distributor/permission/package-info.java
new file mode 100644
index 00000000..6414c90d
--- /dev/null
+++ b/distributor-permission-rank/src/main/java/com/xpdustry/distributor/permission/package-info.java
@@ -0,0 +1,4 @@
+@NullMarked
+package com.xpdustry.distributor.permission;
+
+import org.jspecify.annotations.NullMarked;
diff --git a/settings.gradle.kts b/settings.gradle.kts
index eceb13f3..d9ce1fee 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -4,3 +4,4 @@ includeBuild("distributor-build-logic")
include(":distributor-logging-simple")
include(":distributor-common")
include(":distributor-command-cloud")
+include(":distributor-permission-rank")