This commit is contained in:
kento2 2026-08-19 02:15:15 +02:00
parent 46978ae799
commit ce56c6e105
53 changed files with 776 additions and 891 deletions

View file

@ -1,4 +1,5 @@
build --define=maven_repo=https://git.lab0x13.site/api/packages/scrow/maven build --define=maven_repo=https://git.lab0x13.site/api/packages/scrow/maven
build --define=generic_repo=https://git.lab0x13.site/api/packages/scrow/generic
build --remote_cache=https://cache.lab0x13.site build --remote_cache=https://cache.lab0x13.site
build --remote_upload_local_results=false build --remote_upload_local_results=false

View file

@ -49,7 +49,6 @@ Building
compile the bukkit-impl plugin: compile the bukkit-impl plugin:
```shell ```shell
bazel build //core-bukkit-impl:bukkit-impl_deploy.jar bazel build //core/bukkit:plugin
``` ```
This produces `bazel-bin/core-bukkit-impl/bukkit-impl.jar` with all dependencies built-in except for This produces `bazel-bin/core/bukkit/plugin.jar` I think.
LuckPerms.

View file

@ -1,40 +1,34 @@
# configurator # configurator
helper for generating json configs interactively. This plugin/API allows developers to define configuration fields by name(or rather key) and data type, and have admins easily set
Plugins that hook into this plugin can register configuration nodes like "lobby.spawnpoint" with type Location. the fields' values in-game using commands such as `/cfg key chat.messageDelay set 50 ms`.
server operators can then go ingame and do `/cfg key lobby.spawnpoint` to set a spawn location and then
`/cfg export` ig
## TODO <small>
the key literal in `/cfg key` is required, because there are other modes like `/cfg check` that checks
the config for uninitialized values.
</small>
- lists # Design
- exporting
- proper namespaces
- split into api and impl
- more types(region pickers especially; also itemstacks)
- type variants like location without yaw/pitch
## design Plugins register configuration nodes by key. A key consists of a unique identifier and a type.
The type handles (de)serialization and defines actions. Actions can be used on nodes.
Nodes hold values.
in json we have primitives(like "hi", 1, false"), structures(like {"foo":"bar}) and lists(like []). The `/config` or `/cfg` command is used by admins to configure the plugin:
everything consists of primitives eventually so we can go from A lobby plugin might register a node with the identifier `lobby.spawnLocation` and type `ConfigLocationType`,
which gives us the command `/cfg key lobby.spawnLocation`.
Let's say the ConfigLocationType defines an action to use the player's location as the value("pick-current")
and an action to teleport the user to the defined location("teleport").
Thus, we can do `/cfg key lobby.spawnLocation pick-current` or `/cfg key lobby.spawnLocation teleport`.
Tab completion makes this pretty obvious.
```json ## Lists
{
"arenas": [
{
"name": "skyfall",
"minPlayers": 3,
"maxPlayers": 8
}
]
}
```
to
```text
arenas.0.name = Primitive("skyfall")
arenas.0.minPlayers = Primitive(3)
arenas.0.maxPlayers = Primitive(8)
```
We can define a complex type as "type has a string field called name" and it will just use Now maybe we want to make our lobby have some launch pads(i.e. some pressure plates that launch the player).
So, lists would be handy.
For lists, we simply use a Node of type List<ConfigType of Element>, so for our launch pads, we could make a node
with identifier "lobby.launchpads" and type `ConfigListType<ConfigLocationType>`.
Now to add a launch pad, we can just `/cfg key lobby.launchpads append`, which creates a new uninitialized element
in the list, followed by `/cfg key lobby.launchpads.0 pick-current` which actually sets the node's value.
The node `lobby.launchpads.0` is not a real registered node. It is a Sub-Node of the `lobby.launchpads` node.

View file

@ -0,0 +1,25 @@
package site.lab0x13.scrow.configurator;
import org.jetbrains.annotations.Nullable;
import site.lab0x13.scrow.configurator.node.ConfigNode;
import java.util.Map;
public interface ConfigApi {
<V> void register(ConfigKey<V> key, @Nullable V defaultValue);
default <V> void register(ConfigKey<V> key) {
this.register(key, null);
}
<V> @Nullable ConfigNode<V> getNode(ConfigKey<V> key);
default <V> @Nullable V get(ConfigKey<V> key) {
var n = getNode(key);
if (n == null) return null;
return n.value();
}
Map<String, ConfigNode<?>> allNodes();
}

View file

@ -0,0 +1,75 @@
package site.lab0x13.scrow.configurator;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import site.lab0x13.scrow.configurator.node.ConfigNode;
import java.util.ArrayDeque;
import java.util.HashMap;
import java.util.Map;
public class ConfigApiImpl implements ConfigApi {
private final Logger log = LoggerFactory.getLogger(ConfigApiImpl.class);
private final Map<String, ConfigNode<?>> nodes = new HashMap<>();
@Override
public <V> void register(ConfigKey<V> key, @Nullable V defaultValue) {
var oldValue = nodes.put(key.id(), new ConfigNode<>(key.type(), defaultValue));
if (oldValue != null)
log.warn("Overwriting config key {}", key);
}
/*
Sub-Nodes make this a bit difficult. We need to get the last real registered node in the
identifier first, and then descend into the Sub-Nodes.
*/
@Override
public @Nullable <V> ConfigNode<V> getNode(ConfigKey<V> key) {
ConfigNode<Object> node;
var id = key.id();
var fields = new ArrayDeque<String>();
while (true) {
node = (ConfigNode<Object>) nodes.get(id);
if (node != null)
break;
var dot = id.lastIndexOf('.');
if (dot < 0)
return null;
fields.add(id.substring(dot + 1));
id = id.substring(0, dot);
}
while (!fields.isEmpty()) {
var field = fields.pop();
if (node.value() == null)
return null;
if (node.type().getSubNodeNames(node.value()).isEmpty())
return null;
node = (ConfigNode<Object>) node.type().getSubNode(node.value(), field);
if (node == null)
return null;
}
return (ConfigNode<V>) node;
}
@Override
public Map<String, ConfigNode<?>> allNodes() {
var result = new HashMap<String, ConfigNode<?>>();
nodes.forEach((id, node) -> addNodeRecursively(result, id, (ConfigNode<Object>) node));
return result;
}
private void addNodeRecursively(Map<String, ConfigNode<?>> result, String nodeId, ConfigNode<Object> node) {
result.put(nodeId, node);
if (node.value() != null)
for (String fieldName : node.type().getSubNodeNames(node.value())) {
var subNode = (ConfigNode<Object>) node.type().getSubNode(node.value(), fieldName);
if (subNode == null)
continue;
addNodeRecursively(result, nodeId + "." + fieldName, subNode);
}
}
}

View file

@ -0,0 +1,15 @@
package site.lab0x13.scrow.configurator;
import site.lab0x13.scrow.configurator.type.ConfigType;
/**
* @param id unique identifier with, e.g `foo.bar`
* @param <V> type of the value
*/
public record ConfigKey<V>(String id, ConfigType<V> type) {
@Override
public String toString() {
return "(id=" + id + ",type=" + type.getClass().getCanonicalName() + ")";
}
}

View file

@ -3,8 +3,6 @@ package site.lab0x13.scrow.configurator;
import de.kentoj.scrow.bukkit.ScrowAPI; import de.kentoj.scrow.bukkit.ScrowAPI;
import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.plugin.java.JavaPlugin;
import site.lab0x13.scrow.configurator.command.ConfigCommand; import site.lab0x13.scrow.configurator.command.ConfigCommand;
import site.lab0x13.scrow.configurator.node.ConfigRootNode;
import site.lab0x13.scrow.configurator.node.ConfigValueNode;
import site.lab0x13.scrow.configurator.type.location.LocationConfigType; import site.lab0x13.scrow.configurator.type.location.LocationConfigType;
public class ConfiguratorPlugin extends JavaPlugin { public class ConfiguratorPlugin extends JavaPlugin {
@ -12,7 +10,7 @@ public class ConfiguratorPlugin extends JavaPlugin {
@Override @Override
public void onEnable() { public void onEnable() {
ConfigRootNode nodeRegistry = new ConfigRootNode(); ConfigRootNode nodeRegistry = new ConfigRootNode();
nodeRegistry.register("spawnLocation", new ConfigValueNode<>(LocationConfigType.INSTANCE)); nodeRegistry.register("spawnLocation", new Config<>(LocationConfigType.INSTANCE));
ScrowAPI.playerCommands().register(new ConfigCommand(nodeRegistry).rootLiteral()); ScrowAPI.playerCommands().register(new ConfigCommand(nodeRegistry).rootLiteral());
} }
} }

View file

@ -2,25 +2,20 @@ package site.lab0x13.scrow.configurator.action;
import de.kentoj.kencommandapi.api.argument.CommandArgumentSpec; import de.kentoj.kencommandapi.api.argument.CommandArgumentSpec;
import de.kentoj.kencommandapi.api.invocation.CommandContext; import de.kentoj.kencommandapi.api.invocation.CommandContext;
import de.kentoj.kencommandapi.api.platform.MessageStyle;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import site.lab0x13.scrow.configurator.node.ConfigNode;
import java.util.List; import java.util.List;
public interface ConfigOptionAction<T> { /**
* @param <N> type of ConfigNode
/* FIXME not too clean */ */
List<ConfigOptionAction<?>> GLOBAL_ACTIONS = List.of(new GlobalShowAction<>()); public interface ConfigAction<N extends ConfigNode<?>> {
String name(); String name();
List<CommandArgumentSpec<Player, ?>> arguments(); List<CommandArgumentSpec<Player, ?>> arguments();
boolean requiresValue(); boolean requiresValue();
void execute( void execute(N node, CommandContext<Player> ctx);
MessageStyle style,
T node,
CommandContext<Player> ctx
);
} }

View file

@ -5,16 +5,15 @@ import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
import de.kentoj.kencommandapi.api.argument.CommandArgumentSpec; import de.kentoj.kencommandapi.api.argument.CommandArgumentSpec;
import de.kentoj.kencommandapi.api.invocation.CommandContext; import de.kentoj.kencommandapi.api.invocation.CommandContext;
import de.kentoj.kencommandapi.api.platform.MessageStyle;
import de.kentoj.scrowlib.convention.ScrowMessageStyle;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.event.ClickEvent; import net.kyori.adventure.text.event.ClickEvent;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import site.lab0x13.scrow.configurator.node.ConfigNode; import site.lab0x13.scrow.configurator.node.ConfigNode;
import site.lab0x13.scrow.configurator.type.ConfigType;
import java.util.List; import java.util.List;
public final class GlobalShowAction<S extends ConfigNode<S>> implements ConfigOptionAction<S> { public final class GlobalShowAction implements ConfigAction<ConfigNode<Object>> {
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
@ -34,11 +33,11 @@ public final class GlobalShowAction<S extends ConfigNode<S>> implements ConfigOp
} }
@Override @Override
public void execute(MessageStyle style, S node, CommandContext<Player> ctx) { public void execute(ConfigNode<Object> node, CommandContext<Player> ctx) {
var jsonElement = node.type().serialize(node.value()); var jsonElement = node.type().serialize(node.value());
var prettyJson = GSON.toJson(jsonElement, JsonElement.class); var prettyJson = GSON.toJson(jsonElement, JsonElement.class);
var component = Component.text(prettyJson + " [click to copy]") var component = Component.text(prettyJson + " [click to copy]")
.clickEvent(ClickEvent.copyToClipboard(prettyJson)); .clickEvent(ClickEvent.copyToClipboard(prettyJson));
ctx.sender().sendMessage(style.ok(component)); ctx.sender().sendMessage(ctx.style().ok(component));
} }
} }

View file

@ -2,7 +2,6 @@ package site.lab0x13.scrow.configurator.action;
import de.kentoj.kencommandapi.api.argument.CommandArgumentSpec; import de.kentoj.kencommandapi.api.argument.CommandArgumentSpec;
import de.kentoj.kencommandapi.api.invocation.CommandContext; import de.kentoj.kencommandapi.api.invocation.CommandContext;
import de.kentoj.kencommandapi.api.platform.MessageStyle;
import org.bukkit.Sound; import org.bukkit.Sound;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import site.lab0x13.scrow.configurator.node.ConfigNode; import site.lab0x13.scrow.configurator.node.ConfigNode;
@ -13,9 +12,9 @@ import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
public abstract class PickAction<S, T extends ConfigNode<S>> implements ConfigOptionAction<T> { public abstract class PickAction<V, N extends ConfigNode<V>> implements ConfigAction<N> {
protected abstract CompletableFuture<S> pick(Player player); protected abstract CompletableFuture<V> pick(Player player);
@Override @Override
public String name() { public String name() {
@ -28,16 +27,21 @@ public abstract class PickAction<S, T extends ConfigNode<S>> implements ConfigOp
} }
@Override @Override
public void execute(MessageStyle style, T node, CommandContext<Player> ctx) { public boolean requiresValue() {
return false;
}
@Override
public void execute(N node, CommandContext<Player> ctx) {
pick(ctx.sender()) pick(ctx.sender())
.orTimeout(10, TimeUnit.MINUTES) .orTimeout(10, TimeUnit.MINUTES)
.whenComplete((picked, ex) -> { .whenComplete((picked, ex) -> {
if (ex instanceof TimeoutException) { if (ex instanceof TimeoutException) {
ctx.sender().sendMessage(style.err("Timed out picking value.")); ctx.sender().sendMessage(ctx.style().err("Timed out picking value."));
return;
} }
node.value(picked);
ctx.sender().playSound(ctx.sender().getLocation(), Sound.ENTITY_EXPERIENCE_ORB_PICKUP, 0.5f, 0f); ctx.sender().playSound(ctx.sender().getLocation(), Sound.ENTITY_EXPERIENCE_ORB_PICKUP, 0.5f, 0f);
node.value(picked);
}); });
} }
} }

View file

@ -5,17 +5,17 @@ import de.kentoj.kencommandapi.api.literal.RootLiteral;
import de.kentoj.scrowlib.convention.ScrowMessageStyle; import de.kentoj.scrowlib.convention.ScrowMessageStyle;
import net.kyori.adventure.text.format.TextColor; import net.kyori.adventure.text.format.TextColor;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import site.lab0x13.scrow.configurator.node.ConfigRootNode; import site.lab0x13.scrow.configurator.ConfigApi;
import site.lab0x13.scrow.configurator.noderegistry.ConfigNodeWalker; import site.lab0x13.scrow.configurator.noderegistry.ConfigNodeWalker;
public final class ConfigCommand { public final class ConfigCommand {
private final RootLiteral<Player> rootLiteral; private final RootLiteral<Player> rootLiteral;
public ConfigCommand(ConfigRootNode nodeRegistry) { public ConfigCommand(ConfigApi configApi) {
var style = new ScrowMessageStyle("config", TextColor.color(0x28B088)); var style = new ScrowMessageStyle("config", TextColor.color(0x28B088));
rootLiteral = Literal.<Player>builder("config", "cfg") rootLiteral = Literal.<Player>builder("config", "cfg")
.withPermission("command.config") .withPermission("command.config")
.withLiteral(new KeyLiteral(style, new ConfigNodeWalker(nodeRegistry)).literal()) .withLiteral(new KeyLiteral(style, configApi).literal())
.asRootLiteral(style); .asRootLiteral(style);
} }

View file

@ -5,14 +5,10 @@ import com.leakyabstractions.result.core.Results;
import de.kentoj.kencommandapi.api.argument.ArgumentType; import de.kentoj.kencommandapi.api.argument.ArgumentType;
import de.kentoj.kencommandapi.api.suggestion.SuggestionProvider; import de.kentoj.kencommandapi.api.suggestion.SuggestionProvider;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import site.lab0x13.scrow.configurator.node.ConfigListNode;
import site.lab0x13.scrow.configurator.node.ConfigNode; import site.lab0x13.scrow.configurator.node.ConfigNode;
import site.lab0x13.scrow.configurator.node.ConfigRootNode;
import site.lab0x13.scrow.configurator.node.SubFielded;
import site.lab0x13.scrow.configurator.noderegistry.ConfigNodeWalker; import site.lab0x13.scrow.configurator.noderegistry.ConfigNodeWalker;
import java.util.Collections; import java.util.Collections;
import java.util.stream.IntStream;
public class ConfigNodeArgumentType implements ArgumentType<Player, ConfigNode<?>> { public class ConfigNodeArgumentType implements ArgumentType<Player, ConfigNode<?>> {
@ -37,17 +33,16 @@ public class ConfigNodeArgumentType implements ArgumentType<Player, ConfigNode<?
return (_, input) -> { return (_, input) -> {
var res = nodeWalker.walk(input); var res = nodeWalker.walk(input);
switch (res.type()) { switch (res.type()) {
case NO_FIELDS, SUCCESS -> Collections.emptyList();
case NO_SUCH_FIELD, INDEX_OUT_OF_RANGE, INDEX_EXPECTED -> { case NO_SUCH_FIELD, INDEX_OUT_OF_RANGE, INDEX_EXPECTED -> {
if (res.node() instanceof ConfigListNode<?> list) assert res.node() != null;
return IntStream.range(0, list.size()).mapToObj(Integer::toString).toList(); return res.node().getFieldNames().stream()
if (res.node() instanceof SubFielded subFielded) .map(it -> res.parentKey() + "." + it)
return subFielded.fields().keySet().stream() .toList();
.map(it -> res.parentKey() + "." + it) }
.toList(); default -> {
return Collections.emptyList();
} }
} }
return Collections.emptyList();
}; };
} }
} }

View file

@ -4,9 +4,9 @@ import de.kentoj.kencommandapi.api.literal.Literal;
import de.kentoj.kencommandapi.api.literal.LiteralBuilder; import de.kentoj.kencommandapi.api.literal.LiteralBuilder;
import de.kentoj.scrowlib.convention.ScrowMessageStyle; import de.kentoj.scrowlib.convention.ScrowMessageStyle;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import site.lab0x13.scrow.configurator.action.ConfigOptionAction; import site.lab0x13.scrow.configurator.ConfigApi;
import site.lab0x13.scrow.configurator.action.ConfigAction;
import site.lab0x13.scrow.configurator.node.ConfigNode; import site.lab0x13.scrow.configurator.node.ConfigNode;
import site.lab0x13.scrow.configurator.noderegistry.ConfigNodeWalker;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -16,23 +16,41 @@ class KeyLiteral {
private final ScrowMessageStyle style; private final ScrowMessageStyle style;
private final Literal<Player> literal; private final Literal<Player> literal;
KeyLiteral(ScrowMessageStyle style, ConfigNodeWalker nodeWalker) { KeyLiteral(ScrowMessageStyle style, ConfigApi api) {
/*
All registered nodes should be added as literals. SubNoded nodes should be added
recursively.
When foo is a list node(implements SubNoded):
/cfg key foo <- list node itself is registered, has actions like append, delete, etc.
/cfg key foo.0 <- operate on first element, has actions of foo.0's type
*/
this.style = style; this.style = style;
var builder = Literal.<Player>builder("key"); var builder = Literal.<Player>builder("key");
nodeWalker.getAllNodes().forEach((key, node) -> api.allNodes().forEach((key, node) ->
addNodeLiteral(builder, key, node)); addNodeLiteral(builder, key, node));
literal = builder.build(); this.literal = builder.build();
} }
private void addNodeLiteral(LiteralBuilder<Player> builder, String key, ConfigNode<?> node) { private void addNodeLiteral(LiteralBuilder<Player> builder, String key, ConfigNode<?> node) {
var nodeLiteralBuilder = Literal.<Player>builder(key); var nodeLiteralBuilder = Literal.<Player>builder(key);
addNodeActionLiteral(nodeLiteralBuilder, (ConfigNode<Object>) node, new ArrayList<>(node.type().actions())); addNodeActionLiteral(nodeLiteralBuilder, (ConfigNode<Object>) node, new ArrayList<>(node.type().actions()));
addNodeActionLiteral(nodeLiteralBuilder, (ConfigNode<Object>) node, ConfigOptionAction.GLOBAL_ACTIONS); /*
FUCK ME WE NEED TO ADD LITERALS AT RUNTIME.
I just need /cfg key <node> <actions dependent on node>
except nodes are registered at runtime so we need to register literals at runtime.
or we make it an argument, but then we can't make action's work nicely(only hackishly at best);
or a dy
*/
for (String fieldName : node.type().getSubNodeNames(node.value())) {
var subNode = node.type().getSubNode(node.value(), fieldName);
if (subNode == null) continue;
addNodeActionLiteral();
}
builder.withLiteral(nodeLiteralBuilder.build()); builder.withLiteral(nodeLiteralBuilder.build());
} }
private void addNodeActionLiteral(LiteralBuilder<Player> builder, ConfigNode<Object> node, List<ConfigOptionAction<?>> actions) { private void addNodeActionLiteral(LiteralBuilder<Player> builder, ConfigNode<Object> node, List<ConfigAction<?>> actions) {
for (var _action : actions) { for (var _action : actions) {
var action = (ConfigOptionAction<ConfigNode<Object>>) _action; var action = (ConfigOptionAction<ConfigNode<Object>>) _action;
var actionLiteralBuilder = Literal.<Player>builder(action.name()); var actionLiteralBuilder = Literal.<Player>builder(action.name());
@ -43,7 +61,7 @@ class KeyLiteral {
ctx.sender().sendMessage(style.err("No value set.")); ctx.sender().sendMessage(style.err("No value set."));
return; return;
} }
action.execute(style, node, ctx); action.execute(node, ctx);
}); });
builder.withLiteral(actionLiteralBuilder.build()); builder.withLiteral(actionLiteralBuilder.build());
} }

View file

@ -0,0 +1,56 @@
package site.lab0x13.scrow.configurator.command;
import de.kentoj.kencommandapi.api.argument.CommandArgumentSpec;
import de.kentoj.kencommandapi.api.invocation.CommandExecutor;
import de.kentoj.kencommandapi.api.literal.Literal;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import site.lab0x13.scrow.configurator.node.ConfigNode;
import java.util.List;
public class KeyLiteralImpl implements Literal<Player> {
private final ConfigNode<?> node;
public KeyLiteralImpl(String key, ConfigNode<?> node) {
this.key = key;
this.node = node;
}
@Override
public @Nullable Literal<Player> getLiteral(@NotNull String name) {
node.
return null;
}
@Override
public @NotNull List<Literal<Player>> literals() {
gh
}
@Override
public @NotNull String[] names() {
return new String[]{key};
}
@Override
public @Nullable String description() {
return null;
}
@Override
public List<CommandArgumentSpec<Player, ?>> arguments() {
return List.of();
}
@Override
public @Nullable CommandExecutor<Player> executor() {
return null;
}
@Override
public @Nullable String permission() {
return "";
}
}

View file

@ -1,54 +0,0 @@
package site.lab0x13.scrow.configurator.node;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import site.lab0x13.scrow.configurator.type.ConfigType;
import java.util.ArrayList;
import java.util.List;
public final class ConfigListNode<T>
extends ArrayList<ConfigNode<T>>
implements ConfigNode<List<ConfigNode<T>>>, List<ConfigNode<T>> {
private final ConfigType<ConfigNode<T>> elementType;
public ConfigListNode(ConfigType<ConfigNode<T>> elementType) {
this.elementType = elementType;
}
public ConfigType<ConfigNode<T>> elementType() {
return this.elementType;
}
@Override
public ConfigType<List<ConfigNode<T>>> type() {
return new ConfigType<>() {
@Override
public JsonElement serialize(List<ConfigNode<T>> value) {
var list = new JsonArray();
forEach(n -> list.add(n.type().serialize(n.value())));
return list;
}
@Override
public List<ConfigNode<T>> deserialize(JsonElement json) {
var res = new ArrayList<ConfigNode<T>>();
var list = json.getAsJsonArray();
list.forEach(e -> res.add(elementType.deserialize(e)));
return res;
}
};
}
@Override
public List<ConfigNode<T>> value() {
return this;
}
@Override
public void value(List<ConfigNode<T>> value) {
this.clear();
this.addAll(value);
}
}

View file

@ -1,11 +1,36 @@
package site.lab0x13.scrow.configurator.node; package site.lab0x13.scrow.configurator.node;
import org.jetbrains.annotations.Nullable;
import site.lab0x13.scrow.configurator.type.ConfigType; import site.lab0x13.scrow.configurator.type.ConfigType;
public sealed interface ConfigNode<T> permits ConfigValueNode, ConfigListNode, ConfigRootNode { /**
ConfigType<T> type(); * @param <V> type of the value
*/
public final class ConfigNode<V> {
T value(); private final ConfigType<V> type;
private final @Nullable V defaultValue;
private @Nullable V value;
void value(T value); public ConfigNode(ConfigType<V> type, @Nullable V defaultValue) {
this.type = type;
this.defaultValue = defaultValue;
reset();
}
public void reset() {
this.value(defaultValue);
}
public V value() {
return value;
}
public void value(V value) {
this.value = value;
}
public ConfigType<V> type() {
return type;
}
} }

View file

@ -1,43 +0,0 @@
package site.lab0x13.scrow.configurator.node;
import org.jetbrains.annotations.Nullable;
import site.lab0x13.scrow.configurator.type.ConfigMapType;
import site.lab0x13.scrow.configurator.type.ConfigType;
import java.util.HashMap;
import java.util.Map;
public final class ConfigRootNode implements ConfigNode<Map<String, ConfigNode<?>>>, SubFielded {
private final ConfigMapType<ConfigNode<?>> type = new ConfigMapType<>(new HashMap<>());
private Map<String, ConfigNode<?>> value = new HashMap<>();
public void register(String name, ConfigNode<?> node) {
value.put(name, node);
}
@Override
public ConfigType<Map<String, ConfigNode<?>>> type() {
return type;
}
@Override
public Map<String, ConfigNode<?>> value() {
return value;
}
@Override
public void value(Map<String, ConfigNode<?>> value) {
this.value = value;
}
@Override
public Map<String, ConfigNode<?>> fields() {
return value;
}
@Override
public @Nullable ConfigNode<?> getField(String name) {
return value.get(name);
}
}

View file

@ -1,43 +0,0 @@
package site.lab0x13.scrow.configurator.node;
import site.lab0x13.scrow.configurator.type.ConfigType;
public final class ConfigValueNode<T> implements ConfigNode<T> {
private final ConfigType<T> type;
private final T defaultValue;
private T value;
public ConfigValueNode(ConfigType<T> type, T defaultValue) {
this.type = type;
this.defaultValue = defaultValue;
this.value = defaultValue;
}
public ConfigValueNode(ConfigType<T> type) {
this(type, null);
}
public void reset() {
this.value = defaultValue;
}
public T defaultValue() {
return defaultValue;
}
public T value() {
return value;
}
public void value(T value) {
this.value = value;
}
public boolean required() {
return defaultValue == null;
}
public ConfigType<T> type() {
return type;
}
}

View file

@ -1,11 +0,0 @@
package site.lab0x13.scrow.configurator.node;
import org.jetbrains.annotations.Nullable;
import java.util.Map;
public interface SubFielded {
Map<String, ConfigNode<?>> fields();
@Nullable ConfigNode<?> getField(String name);
}

View file

@ -1,102 +0,0 @@
package site.lab0x13.scrow.configurator.noderegistry;
import org.jetbrains.annotations.Nullable;
import site.lab0x13.scrow.configurator.node.ConfigListNode;
import site.lab0x13.scrow.configurator.node.ConfigNode;
import site.lab0x13.scrow.configurator.node.ConfigRootNode;
import site.lab0x13.scrow.configurator.node.SubFielded;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
public final class ConfigNodeWalker {
private final ConfigRootNode rootNode;
public ConfigNodeWalker(ConfigRootNode rootNode) {
this.rootNode = rootNode;
}
public Map<String, ConfigNode<?>> getAllNodes() {
var result = new HashMap<String, ConfigNode<?>>();
processNode("", result, rootNode);
return result;
}
private void processNode(String key, Map<String, ConfigNode<?>> result, ConfigNode<?> node) {
result.put(key, node);
if (node instanceof ConfigListNode<?> list) {
for (int i = 0; i < list.size(); i++) {
var element = list.get(i);
processNode(key + "." + i, result, element);
}
} else {
/* FIXME not clean at all */
SubFielded sf = null;
if (node instanceof SubFielded n)
sf = n;
else if (node.type() instanceof SubFielded n)
sf = n;
if (sf != null)
sf.fields().forEach((name, v) ->
processNode(key + "." + name, result, v));
}
}
public WalkResult walk(String key) {
return walk(Arrays.stream(key.split("\\.")).iterator());
}
public WalkResult walk(Iterator<String> iter) {
var cursor = new Cursor(iter);
ConfigNode<?> cur = rootNode;
String token;
while ((token = cursor.nextToken()) != null) {
if (token.isEmpty()) continue;
if (cur instanceof ConfigListNode<?> list) {
int index;
try {
index = Integer.parseInt(token);
} catch (NumberFormatException _) {
return new WalkResult(WalkResult.Type.INDEX_EXPECTED, cursor.key(), cur);
}
if (index < 0 || index >= list.size())
return new WalkResult(WalkResult.Type.INDEX_OUT_OF_RANGE, cursor.key(), cur);
cur = list.get(index);
} else if (cur.type() instanceof SubFielded subFielded) {
cur = subFielded.getField(token);
if (cur == null)
return new WalkResult(WalkResult.Type.NO_SUCH_FIELD, cursor.key(), cur);
} else {
return new WalkResult(WalkResult.Type.NO_FIELDS, cursor.key(), cur);
}
}
return new WalkResult(WalkResult.Type.SUCCESS, cursor.key(), cur);
}
private static class Cursor {
private String key = "";
private final Iterator<String> iter;
private Cursor(Iterator<String> iter) {
this.iter = iter;
}
public @Nullable String nextToken() {
if (!iter.hasNext()) return null;
var token = iter.next();
key += "." + token;
key = key.replaceFirst("\\.", "");
return token;
}
public String key() {
return key;
}
}
}

View file

@ -1,35 +0,0 @@
package site.lab0x13.scrow.configurator.noderegistry;
import org.jetbrains.annotations.Nullable;
import site.lab0x13.scrow.configurator.node.ConfigNode;
import java.util.function.Function;
public record WalkResult(
Type type,
String key,
@Nullable ConfigNode<?> node
) {
public String parentKey() {
var dotInd = key.lastIndexOf('.');
return dotInd < 0 ? "" : key.substring(0, dotInd - 1);
}
public @Nullable String error() {
return type().message.apply(this);
}
public enum Type {
INDEX_EXPECTED(r -> "Numeric index expected at " + r.key() + "."),
INDEX_OUT_OF_RANGE(_ -> "Index out of range."),
NO_FIELDS(r -> r.key() + " has no fields."),
NO_SUCH_FIELD(r -> "Invalid key " + r.key()),
SUCCESS(_ -> null);
private final Function<WalkResult, String> message;
Type(Function<WalkResult, String> message) {
this.message = message;
}
}
}

View file

@ -2,27 +2,24 @@ package site.lab0x13.scrow.configurator.type;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import org.jetbrains.annotations.Nullable;
import site.lab0x13.scrow.configurator.node.SubFielded;
import site.lab0x13.scrow.configurator.node.ConfigNode;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.function.Function;
public abstract class ConfigComplexType<T> implements ConfigType<T>, SubFielded { public abstract class ConfigComplexType<T> implements ConfigType<T> {
private final Map<String, ConfigNode<?>> entries = new HashMap<>(); private final Map<String, Field<T, ?>> fields = new HashMap<>();
protected abstract T deserialize(JsonObject obj); protected abstract T deserialize(JsonObject obj);
protected void addEntry(String name, ConfigNode<?> node) { protected <S> void addField(String name, ConfigType<S> type, Function<T, S> getter) {
entries.put(name, node); fields.put(name, new Field<>(type, getter));
} }
@SuppressWarnings("unchecked") protected <S> S readField(JsonObject object, String name) {
protected <S> S readNode(JsonObject object, String name) { var field = fields.get(name);
var node = (ConfigNode<Object>) entries.get(name); return (S) field.type().deserialize(object.get(name));
return (S) node.type().deserialize(object.get(name));
} }
@Override @Override
@ -33,20 +30,19 @@ public abstract class ConfigComplexType<T> implements ConfigType<T>, SubFielded
@Override @Override
public JsonElement serialize(T value) { public JsonElement serialize(T value) {
var obj = new JsonObject(); var obj = new JsonObject();
entries.forEach((name, node) -> writeNode(obj, name, node)); fields.forEach((name, field) -> writeNode(obj, name, field, value));
return obj; return obj;
} }
private static <S> void writeNode(JsonObject object, String name, ConfigNode<S> node) { private <S> void writeNode(JsonObject object, String name, Field<T, S> field, T value) {
var value = node.type().serialize(node.value()); var fieldValue = field.getter.apply(value);
object.add(name, value); var serializedFieldValue = field.type().serialize(fieldValue);
object.add(name, serializedFieldValue);
} }
public Map<String, ConfigNode<?>> fields() { public record Field<T, S>(
return new HashMap<>(entries); ConfigType<S> type,
} Function<T, S> getter
) {
public @Nullable ConfigNode<?> getField(String name) {
return entries.get(name);
} }
} }

View file

@ -0,0 +1,51 @@
package site.lab0x13.scrow.configurator.type;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import org.jetbrains.annotations.Nullable;
import site.lab0x13.scrow.configurator.node.ConfigNode;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.IntStream;
public record ConfigListType<V>(
ConfigType<V> elementType
) implements ConfigType<List<ConfigNode<V>>> {
@Override
public List<ConfigNode<V>> deserialize(JsonElement json) {
var result = new ArrayList<ConfigNode<V>>();
var obj = json.getAsJsonObject();
obj.asMap().forEach((k, v) -> {
var subNode = new ConfigNode<>(elementType, null);
subNode.value(elementType.deserialize(v));
result.set(Integer.parseInt(k), subNode);
});
return result;
}
@Override
public JsonElement serialize(List<ConfigNode<V>> value) {
var res = new JsonObject();
for (int i = 0; i < value.size(); i++) {
var subNodeValue = value.get(i).value();
if (subNodeValue == null) continue;
var element = elementType.serialize(subNodeValue);
res.add(Integer.toString(i), element);
}
return res;
}
@Override
public List<String> getSubNodeNames(List<ConfigNode<V>> value) {
return IntStream.range(0, value.size())
.mapToObj(Integer::toString)
.toList();
}
@Override
public @Nullable ConfigNode<V> getSubNode(List<ConfigNode<V>> value, String name) {
return value.get(Integer.parseInt(name));
}
}

View file

@ -1,54 +0,0 @@
package site.lab0x13.scrow.configurator.type;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import org.jetbrains.annotations.Nullable;
import site.lab0x13.scrow.configurator.node.ConfigNode;
import site.lab0x13.scrow.configurator.node.SubFielded;
import java.util.HashMap;
import java.util.Map;
public class ConfigMapType<T> implements ConfigType<Map<String, T>>, SubFielded {
private final Map<String, ConfigType<?>> types;
public ConfigMapType(Map<String, ConfigType<?>> types) {
this.types = types;
}
@SuppressWarnings("unchecked")
@Override
public Map<String, T> deserialize(JsonElement json) {
var result = new HashMap<String, T>();
var obj = json.getAsJsonObject();
types.forEach((k, v) -> {
var type = (ConfigType<Object>) v;
var value = (T) type.deserialize(obj.get(k));
result.put(k, value);
});
return result;
}
@SuppressWarnings("unchecked")
@Override
public JsonElement serialize(Map<String, T> value) {
var res = new JsonObject();
types.forEach((k, v) -> {
var type = (ConfigType<T>) v;
var serialized = type.serialize(value.get(k));
res.add(k, serialized);
});
return res;
}
@Override
public Map<String, ConfigNode<?>> fields() {
return Map.of();
}
@Override
public @Nullable ConfigNode<?> getField(String name) {
return null;
}
}

View file

@ -6,7 +6,7 @@ import com.google.gson.JsonPrimitive;
import java.math.BigInteger; import java.math.BigInteger;
import java.util.function.Function; import java.util.function.Function;
public class ConfigPrimitiveType<T> implements ConfigType<T> { public final class ConfigPrimitiveType<V> implements ConfigType<V> {
public static final ConfigPrimitiveType<Double> DOUBLE = public static final ConfigPrimitiveType<Double> DOUBLE =
new ConfigPrimitiveType<>(JsonPrimitive::new, JsonPrimitive::getAsDouble); new ConfigPrimitiveType<>(JsonPrimitive::new, JsonPrimitive::getAsDouble);
@ -25,21 +25,21 @@ public class ConfigPrimitiveType<T> implements ConfigType<T> {
public static final ConfigPrimitiveType<Byte> BYTE = public static final ConfigPrimitiveType<Byte> BYTE =
new ConfigPrimitiveType<>(JsonPrimitive::new, JsonPrimitive::getAsByte); new ConfigPrimitiveType<>(JsonPrimitive::new, JsonPrimitive::getAsByte);
private final Function<T, JsonPrimitive> serializer; private final Function<V, JsonPrimitive> serializer;
private final Function<JsonPrimitive, T> deserializer; private final Function<JsonPrimitive, V> deserializer;
private ConfigPrimitiveType(Function<T, JsonPrimitive> serializer, Function<JsonPrimitive, T> deserializer) { private ConfigPrimitiveType(Function<V, JsonPrimitive> serializer, Function<JsonPrimitive, V> deserializer) {
this.serializer = serializer; this.serializer = serializer;
this.deserializer = deserializer; this.deserializer = deserializer;
} }
@Override @Override
public JsonElement serialize(T value) { public JsonElement serialize(V value) {
return serializer.apply(value); return serializer.apply(value);
} }
@Override @Override
public T deserialize(JsonElement json) { public V deserialize(JsonElement json) {
return deserializer.apply(json.getAsJsonPrimitive()); return deserializer.apply(json.getAsJsonPrimitive());
} }
} }

View file

@ -1,28 +1,26 @@
package site.lab0x13.scrow.configurator.type; package site.lab0x13.scrow.configurator.type;
import com.google.gson.JsonElement; import org.jetbrains.annotations.Nullable;
import com.google.gson.JsonParser; import site.lab0x13.scrow.configurator.action.ConfigAction;
import site.lab0x13.scrow.configurator.action.ConfigOptionAction;
import site.lab0x13.scrow.configurator.node.ConfigNode; import site.lab0x13.scrow.configurator.node.ConfigNode;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
public interface ConfigType<T> { /**
* @param <V> type of the stored value
*/
public interface ConfigType<V> extends Serializable<V> {
default List<ConfigOptionAction<?>> actions() { default List<ConfigAction<?>> actions() {
return Collections.emptyList(); return Collections.emptyList();
} }
JsonElement serialize(T value); default List<String> getSubNodeNames(V value) {
return Collections.emptyList();
T deserialize(JsonElement json);
default String toJson(T value) {
return serialize(value).toString();
} }
default T fromJson(String json) { default @Nullable ConfigNode<?> getSubNode(V value, String name) {
return deserialize(JsonParser.parseString(json)); return null;
} }
} }

View file

@ -0,0 +1,19 @@
package site.lab0x13.scrow.configurator.type;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
public interface Serializable<V> {
JsonElement serialize(V value);
V deserialize(JsonElement json);
default String toJson(V value) {
return serialize(value).toString();
}
default V fromJson(String json) {
return deserialize(JsonParser.parseString(json));
}
}

View file

@ -2,9 +2,7 @@ package site.lab0x13.scrow.configurator.type.location;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import org.bukkit.Location; import org.bukkit.Location;
import site.lab0x13.scrow.configurator.action.ConfigOptionAction; import site.lab0x13.scrow.configurator.action.ConfigAction;
import site.lab0x13.scrow.configurator.node.ConfigNode;
import site.lab0x13.scrow.configurator.node.ConfigValueNode;
import site.lab0x13.scrow.configurator.type.ConfigComplexType; import site.lab0x13.scrow.configurator.type.ConfigComplexType;
import site.lab0x13.scrow.configurator.type.ConfigPrimitiveType; import site.lab0x13.scrow.configurator.type.ConfigPrimitiveType;
import site.lab0x13.scrow.configurator.type.world.WorldConfigType; import site.lab0x13.scrow.configurator.type.world.WorldConfigType;
@ -16,31 +14,28 @@ public class LocationConfigType extends ConfigComplexType<Location> {
public static final LocationConfigType INSTANCE = new LocationConfigType(); public static final LocationConfigType INSTANCE = new LocationConfigType();
private LocationConfigType() { private LocationConfigType() {
addEntry("world", new ConfigValueNode<>(WorldConfigType.INSTANCE)); addField("world", WorldConfigType.INSTANCE, Location::getWorld);
addEntry("x", new ConfigValueNode<>(ConfigPrimitiveType.DOUBLE)); addField("x", ConfigPrimitiveType.DOUBLE, Location::getX);
addEntry("y", new ConfigValueNode<>(ConfigPrimitiveType.DOUBLE)); addField("y", ConfigPrimitiveType.DOUBLE, Location::getY);
addEntry("z", new ConfigValueNode<>(ConfigPrimitiveType.DOUBLE)); addField("z", ConfigPrimitiveType.DOUBLE, Location::getZ);
addEntry("yaw", new ConfigValueNode<>(ConfigPrimitiveType.FLOAT)); addField("yaw", ConfigPrimitiveType.FLOAT, Location::getYaw);
addEntry("pitch", new ConfigValueNode<>(ConfigPrimitiveType.FLOAT)); addField("pitch", ConfigPrimitiveType.FLOAT, Location::getPitch);
} }
@Override @Override
public List<ConfigOptionAction<?>> actions() { public List<ConfigAction<?>> actions() {
return List.of( return List.of(new LocationTeleportAction(), new LocationPickCurrentAction());
new LocationTeleportAction(),
new LocationPickCurrentAction()
);
} }
@Override @Override
public Location deserialize(JsonObject obj) { public Location deserialize(JsonObject obj) {
return new Location( return new Location(
readNode(obj, "world"), readField(obj, "world"),
readNode(obj, "x"), readField(obj, "x"),
readNode(obj, "y"), readField(obj, "y"),
readNode(obj, "z"), readField(obj, "z"),
readNode(obj, "yaw"), readField(obj, "yaw"),
readNode(obj, "pitch") readField(obj, "pitch")
); );
} }
} }

View file

@ -13,11 +13,6 @@ class LocationPickCurrentAction extends PickAction<Location, ConfigNode<Location
return "pick-current"; return "pick-current";
} }
@Override
public boolean requiresValue() {
return false;
}
@Override @Override
protected CompletableFuture<Location> pick(Player player) { protected CompletableFuture<Location> pick(Player player) {
return CompletableFuture.completedFuture(player.getLocation()); return CompletableFuture.completedFuture(player.getLocation());

View file

@ -2,17 +2,15 @@ package site.lab0x13.scrow.configurator.type.location;
import de.kentoj.kencommandapi.api.argument.CommandArgumentSpec; import de.kentoj.kencommandapi.api.argument.CommandArgumentSpec;
import de.kentoj.kencommandapi.api.invocation.CommandContext; import de.kentoj.kencommandapi.api.invocation.CommandContext;
import de.kentoj.kencommandapi.api.platform.MessageStyle;
import de.kentoj.scrowlib.convention.ScrowMessageStyle;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import site.lab0x13.scrow.configurator.action.ConfigOptionAction; import site.lab0x13.scrow.configurator.action.ConfigAction;
import site.lab0x13.scrow.configurator.node.ConfigValueNode; import site.lab0x13.scrow.configurator.node.ConfigNode;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
class LocationTeleportAction implements ConfigOptionAction<ConfigValueNode<Location>> { class LocationTeleportAction implements ConfigAction<ConfigNode<Location>> {
@Override @Override
public String name() { public String name() {
@ -30,8 +28,8 @@ class LocationTeleportAction implements ConfigOptionAction<ConfigValueNode<Locat
} }
@Override @Override
public void execute(MessageStyle style, ConfigValueNode<Location> node, CommandContext<Player> ctx) { public void execute(ConfigNode<Location> node, CommandContext<Player> ctx) {
ctx.sender().teleport(node.value()); ctx.sender().teleport(node.value());
ctx.sender().sendMessage(style.ok("You've been teleported.")); ctx.sender().sendMessage(ctx.style().ok("You've been teleported."));
} }
} }

View file

@ -0,0 +1,39 @@
package site.lab0x13.scrow.configurator.type.world;
import de.kentoj.kencommandapi.api.argument.CommandArgumentSpec;
import de.kentoj.kencommandapi.api.invocation.CommandContext;
import de.kentoj.kencommandapi.type.WorldArgumentType;
import org.bukkit.World;
import org.bukkit.entity.Player;
import site.lab0x13.scrow.configurator.action.ConfigAction;
import site.lab0x13.scrow.configurator.node.ConfigNode;
import java.util.List;
class SetWorldAction implements ConfigAction<ConfigNode<World>> {
private final CommandArgumentSpec<Player, World> arg =
CommandArgumentSpec.builder("world", new WorldArgumentType<Player>()).build();
@Override
public String name() {
return "set";
}
@Override
public List<CommandArgumentSpec<Player, ?>> arguments() {
return List.of(arg);
}
@Override
public boolean requiresValue() {
return false;
}
@Override
public void execute(ConfigNode<World> node, CommandContext<Player> ctx) {
World world = ctx.getArg(arg);
node.value(world);
ctx.sender().sendMessage(ctx.style().ok("Set world to " + world.getName() + "(" + world.getUID() + ")"));
}
}

View file

@ -2,18 +2,9 @@ package site.lab0x13.scrow.configurator.type.world;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
import com.google.gson.JsonPrimitive; import com.google.gson.JsonPrimitive;
import com.leakyabstractions.result.api.Result;
import com.leakyabstractions.result.core.Results;
import de.kentoj.kencommandapi.api.argument.CommandArgumentSpec;
import de.kentoj.kencommandapi.api.invocation.CommandContext;
import de.kentoj.kencommandapi.api.platform.MessageStyle;
import de.kentoj.kencommandapi.type.WorldArgumentType;
import de.kentoj.scrowlib.convention.ScrowMessageStyle;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.World; import org.bukkit.World;
import org.bukkit.entity.Player; import site.lab0x13.scrow.configurator.action.ConfigAction;
import site.lab0x13.scrow.configurator.action.ConfigOptionAction;
import site.lab0x13.scrow.configurator.node.ConfigValueNode;
import site.lab0x13.scrow.configurator.type.ConfigType; import site.lab0x13.scrow.configurator.type.ConfigType;
import java.util.List; import java.util.List;
@ -37,33 +28,7 @@ public class WorldConfigType implements ConfigType<World> {
} }
@Override @Override
public List<ConfigOptionAction<?>> actions() { public List<ConfigAction<?>> actions() {
return List.of(new SetAction()); return List.of(new SetWorldAction());
}
private static class SetAction implements ConfigOptionAction<ConfigValueNode<World>> {
@Override
public String name() {
return "set";
}
@Override
public List<CommandArgumentSpec<Player, ?>> arguments() {
return List.of(
CommandArgumentSpec.builder("world", new WorldArgumentType<Player>()).build()
);
}
@Override
public boolean requiresValue() {
return false;
}
@Override
public void execute(MessageStyle style, ConfigValueNode<World> node, CommandContext<Player> ctx) {
World world = ctx.getArg("world");
node.value(world);
ctx.sender().sendMessage(style.ok("Set world to " + world.getName() + "("+ world.getUID() + ")"));
}
} }
} }

View file

@ -43,4 +43,12 @@ genrule(
outs = ["plugin.jar"], outs = ["plugin.jar"],
cmd = "cp $< $@", cmd = "cp $< $@",
visibility = ["//visibility:public"], visibility = ["//visibility:public"],
)
load("//rules:publish_plugin.bzl", "publish_plugin")
publish_plugin(
name = "plugin-publish",
package = "core-bukkit",
filename = "CoreBukkit.jar",
src = ":plugin",
) )

View file

@ -2,10 +2,6 @@ package de.kentoj.scrow.bukkit;
import de.kentoj.scrow.bukkit.command.economy.CoinsCommand; import de.kentoj.scrow.bukkit.command.economy.CoinsCommand;
import de.kentoj.scrow.bukkit.command.friends.FriendCommand; import de.kentoj.scrow.bukkit.command.friends.FriendCommand;
import de.kentoj.scrow.bukkit.command.instance.InstanceCache;
import de.kentoj.scrow.bukkit.command.instance.InstanceCommand;
import de.kentoj.scrow.bukkit.command.instance.LobbyCommand;
import de.kentoj.scrow.bukkit.command.instance.PlayCommand;
import de.kentoj.scrow.bukkit.internal.player.prefix.CachedPrefixProvider; import de.kentoj.scrow.bukkit.internal.player.prefix.CachedPrefixProvider;
import de.kentoj.scrow.bukkit.internal.player.prefix.LuckPermsPrefixSetter; import de.kentoj.scrow.bukkit.internal.player.prefix.LuckPermsPrefixSetter;
import de.kentoj.scrow.bukkit.style.ChatStyleListener; import de.kentoj.scrow.bukkit.style.ChatStyleListener;
@ -27,10 +23,6 @@ public final class CoreImplPlugin extends JavaPlugin {
ScrowAPI.playerCommands().register(new CoinsCommand().rootLiteral()); ScrowAPI.playerCommands().register(new CoinsCommand().rootLiteral());
ScrowAPI.playerCommands().register(new FriendCommand().rootLiteral()); ScrowAPI.playerCommands().register(new FriendCommand().rootLiteral());
var instanceCache = new InstanceCache();
ScrowAPI.commands().register(new InstanceCommand(instanceCache).rootLiteral());
ScrowAPI.playerCommands().register(new PlayCommand().rootLiteral());
ScrowAPI.playerCommands().register(new LobbyCommand().rootLiteral());
var luckperms = LuckPermsProvider.get(); var luckperms = LuckPermsProvider.get();
new LuckPermsPrefixSetter(luckperms, cachedPrefixProvider).init(this); new LuckPermsPrefixSetter(luckperms, cachedPrefixProvider).init(this);

View file

@ -1,40 +0,0 @@
package de.kentoj.scrow.bukkit.command.instance;
import de.kentoj.scrow.bukkit.ScrowAPI;
import de.kentoj.scrowlib.instancemanager.ServerInstance;
import org.bson.Document;
import org.jetbrains.annotations.Nullable;
import java.util.Collection;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
public class InstanceCache {
private final ConcurrentMap<String, ServerInstance> cache = new ConcurrentHashMap<>();
public InstanceCache() {
ScrowAPI.messageBroker().subscribe("event.instance.up", doc -> {
var inst = toInstance(doc);
cache.put(inst.handle(), inst);
});
ScrowAPI.messageBroker().subscribe("event.instance.down", doc ->
cache.remove(doc.getString("handle")));
}
public Collection<ServerInstance> getInstances() {
return cache.values();
}
public @Nullable ServerInstance getInstance(String handle) {
return cache.get(handle);
}
private ServerInstance toInstance(Document doc) {
return new ServerInstance(
doc.getString("handle"),
doc.getString("template"),
doc.getInteger("port")
);
}
}

View file

@ -1,24 +0,0 @@
package de.kentoj.scrow.bukkit.command.instance;
import de.kentoj.kencommandapi.api.literal.Literal;
import de.kentoj.kencommandapi.api.literal.RootLiteral;
import de.kentoj.scrowlib.convention.ScrowMessageStyle;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.command.CommandSender;
public final class InstanceCommand {
private final RootLiteral<CommandSender> rootLiteral;
public InstanceCommand(InstanceCache instanceCache) {
rootLiteral = Literal.<CommandSender>builder("instance", "server-manager")
.withLiteral(new InstanceListLiteral().literal())
.withLiteral(new InstanceDeployLiteral().literal())
.withLiteral(new InstanceDestroyLiteral(instanceCache).literal())
.asRootLiteral(new ScrowMessageStyle("server-manager", NamedTextColor.RED));
}
public RootLiteral<CommandSender> rootLiteral() {
return rootLiteral;
}
}

View file

@ -1,41 +0,0 @@
package de.kentoj.scrow.bukkit.command.instance;
import de.kentoj.kencommandapi.api.argument.CommandArgumentSpec;
import de.kentoj.kencommandapi.api.argument.types.StringArgumentType;
import de.kentoj.kencommandapi.api.invocation.CommandContext;
import de.kentoj.kencommandapi.api.literal.Literal;
import de.kentoj.scrow.bukkit.ScrowAPI;
import de.kentoj.scrow.bukkit.scheduler.Tasks;
import org.bukkit.command.CommandSender;
import java.util.concurrent.CompletableFuture;
public final class InstanceDeployLiteral {
private final Literal<CommandSender> literal;
private final CommandArgumentSpec<CommandSender, String> templateArg;
InstanceDeployLiteral() {
templateArg = CommandArgumentSpec.builder("template", new StringArgumentType<CommandSender>())
.build();
literal = Literal.<CommandSender>builder("deploy")
.withArgument(templateArg)
.withExecutor(this::deployInstance)
.build();
}
private CompletableFuture<?> deployInstance(CommandContext<CommandSender> ctx) {
var template = ctx.getArg(templateArg);
ctx.sender().sendMessage(ctx.style().ok("Deploying instance... this may take a while."));
return Tasks.supplyAsync(() ->
ScrowAPI.instanceManager().deployInstance(template))
.thenSync(instance -> {
ctx.sender().sendMessage(ctx.style().ok("Deployed instance of " + template + " with handle " + instance.handle() + "."));
// FIXME handle unknown server
}).toFuture();
}
public Literal<CommandSender> literal() {
return literal;
}
}

View file

@ -1,48 +0,0 @@
package de.kentoj.scrow.bukkit.command.instance;
import de.kentoj.kencommandapi.api.argument.CommandArgumentSpec;
import de.kentoj.kencommandapi.api.argument.types.StringArgumentType;
import de.kentoj.kencommandapi.api.invocation.CommandContext;
import de.kentoj.kencommandapi.api.literal.Literal;
import de.kentoj.scrow.bukkit.ScrowAPI;
import de.kentoj.scrow.bukkit.scheduler.Tasks;
import de.kentoj.scrowlib.convention.ScrowMessageStyle;
import de.kentoj.scrowlib.instancemanager.ServerInstance;
import org.bukkit.command.CommandSender;
import java.util.concurrent.CompletableFuture;
public final class InstanceDestroyLiteral {
private final Literal<CommandSender> literal;
private final CommandArgumentSpec<CommandSender, String> handleArg;
InstanceDestroyLiteral(InstanceCache cache) {
handleArg = CommandArgumentSpec.builder("handle", new StringArgumentType<CommandSender>())
.withSuggestionProvider((_, _) ->
cache.getInstances().stream()
.map(ServerInstance::handle)
.toList())
.build();
literal = Literal.<CommandSender>builder("destroy")
.withArgument(handleArg)
.withExecutor(this::destroyInstance)
.build();
}
private CompletableFuture<?> destroyInstance(CommandContext<CommandSender> ctx) {
var handle = ctx.getArg(handleArg);
return Tasks.supplyAsync(() -> ScrowAPI.instanceManager().destroyInstance(handle))
.thenSync(found -> {
if (!found) {
ctx.sender().sendMessage(ctx.style().err("No instance with that handle found."));
return;
}
ctx.sender().sendMessage(ctx.style().ok("Destroyed server with handle " + handle + "."));
}).toFuture();
}
public Literal<CommandSender> literal() {
return literal;
}
}

View file

@ -1,38 +0,0 @@
package de.kentoj.scrow.bukkit.command.instance;
import de.kentoj.kencommandapi.api.invocation.CommandContext;
import de.kentoj.kencommandapi.api.literal.Literal;
import de.kentoj.scrow.bukkit.ScrowAPI;
import de.kentoj.scrow.bukkit.scheduler.Tasks;
import net.kyori.adventure.text.Component;
import org.bukkit.command.CommandSender;
import java.util.concurrent.CompletableFuture;
public final class InstanceListLiteral {
private final Literal<CommandSender> literal;
InstanceListLiteral() {
this.literal = Literal.<CommandSender>builder("list")
.withExecutor(this::displayInstanceList)
.build();
}
private CompletableFuture<?> displayInstanceList(CommandContext<CommandSender> ctx) {
return Tasks.supplyAsync(() ->
ScrowAPI.instanceManager().instances())
.thenSync(instances -> {
var msg = Component.text("Running instances:");
for (var inst : instances) {
msg = msg.appendNewline()
.append(Component.text(" - " + inst.handle() + " on port " + inst.port()));
}
ctx.sender().sendMessage(ctx.style().ok(msg));
}).toFuture();
}
public Literal<CommandSender> literal() {
return literal;
}
}

View file

@ -1,29 +0,0 @@
package de.kentoj.scrow.bukkit.command.instance;
import com.leakyabstractions.result.api.Result;
import com.leakyabstractions.result.core.Results;
import de.kentoj.kencommandapi.api.invocation.CommandContext;
import de.kentoj.kencommandapi.api.literal.Literal;
import de.kentoj.kencommandapi.api.literal.RootLiteral;
import de.kentoj.scrowlib.convention.ScrowMessageStyle;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
public final class LobbyCommand {
private final RootLiteral<Player> rootLiteral;
public LobbyCommand() {
rootLiteral = Literal.<Player>builder("lobby", "l", "hub")
.withSyncExecutor(this::sendToLobby)
.asRootLiteral(ScrowMessageStyle.GENERIC);
}
private void sendToLobby(CommandContext<Player> ctx) {
Bukkit.dispatchCommand(ctx.sender(), "play lobby");
}
public RootLiteral<Player> rootLiteral() {
return rootLiteral;
}
}

View file

@ -1,54 +0,0 @@
package de.kentoj.scrow.bukkit.command.instance;
import de.kentoj.kencommandapi.api.argument.CommandArgumentSpec;
import de.kentoj.kencommandapi.api.argument.types.StringArgumentType;
import de.kentoj.kencommandapi.api.invocation.CommandContext;
import de.kentoj.kencommandapi.api.literal.Literal;
import de.kentoj.kencommandapi.api.literal.RootLiteral;
import de.kentoj.scrow.bukkit.ScrowAPI;
import de.kentoj.scrow.bukkit.scheduler.Tasks;
import de.kentoj.scrowlib.convention.ScrowMessageStyle;
import org.bukkit.entity.Player;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
public final class PlayCommand {
private final RootLiteral<Player> rootLiteral;
private final CommandArgumentSpec<Player, String> gamemodeArg;
public PlayCommand() {
var style = ScrowMessageStyle.GENERIC;
gamemodeArg = CommandArgumentSpec.builder("gamemode", new StringArgumentType<Player>()).build();
rootLiteral = Literal.<Player>builder("play", "queue")
.withArgument(gamemodeArg)
.withExecutor(this::execute)
.asRootLiteral(style);
}
private CompletableFuture<?> execute(CommandContext<Player> ctx) {
var gamemode = ctx.getArg(gamemodeArg);
return Tasks.supplyAsync(() -> ScrowAPI.instanceManager().instances())
.mapSync(instances -> {
var instance = instances.stream()
.filter(inst -> inst.template().equals(gamemode))
.findFirst()
.orElse(null);
if (instance == null) {
ctx.sender().sendMessage(ctx.style().err("No instance for " + gamemode + " found."));
return null;
}
return instance.handle();
})
.yieldIf(Objects::isNull)
.thenAsync(handle ->
ScrowAPI.getPlayer(ctx.sender()).sendToInstance(handle)
).toFuture();
}
public RootLiteral<Player> rootLiteral() {
return rootLiteral;
}
}

View file

@ -1,5 +1,5 @@
name: "CoreBukkit" name: "CoreBukkit"
version: "${version}" version: "UNVERSIONED"
depend: [ "LuckPerms" ] depend: [ "LuckPerms" ]
main: "de.kentoj.scrow.bukkit.CoreImplPlugin" main: "de.kentoj.scrow.bukkit.CoreImplPlugin"
api-version: "1.21" api-version: "1.21"

View file

@ -1,6 +1,7 @@
load("@rules_jvm_external//:defs.bzl", "artifact") load("@rules_jvm_external//:defs.bzl", "artifact")
load("@rules_java//java:java_library.bzl", "java_library") load("@rules_java//java:java_library.bzl", "java_library")
load("@rules_java//java:java_binary.bzl", "java_binary") load("@rules_java//java:java_binary.bzl", "java_binary")
load("@rules_jvm_external//:defs.bzl", "artifact", "java_plugin_artifact")
java_library( java_library(
name = "api", name = "api",
@ -20,7 +21,7 @@ java_library(
) )
java_binary( java_binary(
name = "plugin", name = "_plugin",
srcs = glob(["plugin/main/java/**/*.java", "api/main/java/**/*.java"]), srcs = glob(["plugin/main/java/**/*.java", "api/main/java/**/*.java"]),
create_executable = False, create_executable = False,
deps = [ deps = [
@ -30,5 +31,29 @@ java_binary(
artifact("io.nats:jnats"), artifact("io.nats:jnats"),
artifact("de.kentoj.scrow:kencommandapi-velocity"), artifact("de.kentoj.scrow:kencommandapi-velocity"),
artifact("org.mongodb:bson"), artifact("org.mongodb:bson"),
artifact("org.postgresql:postgresql"),
artifact("com.google.code.gson:gson"),
],
plugins = [
java_plugin_artifact(
"com.velocitypowered:velocity-api",
"com.velocitypowered.api.plugin.ap.PluginAnnotationProcessor",
),
], ],
) )
genrule(
name = "plugin",
srcs = [":_plugin_deploy.jar"],
outs = ["plugin.jar"],
cmd = "cp $< $@",
visibility = ["//visibility:public"],
)
load("//rules:publish_plugin.bzl", "publish_plugin")
publish_plugin(
name = "plugin-publish",
package = "core-velocity",
filename = "CoreVelocity.jar",
src = ":plugin",
)

View file

@ -6,17 +6,21 @@ import com.velocitypowered.api.event.proxy.ProxyInitializeEvent;
import com.velocitypowered.api.plugin.Plugin; import com.velocitypowered.api.plugin.Plugin;
import com.velocitypowered.api.proxy.ProxyServer; import com.velocitypowered.api.proxy.ProxyServer;
import de.kentoj.scrow.corevelocity.misc.OnlineCommand; import de.kentoj.scrow.corevelocity.misc.OnlineCommand;
import de.kentoj.scrow.corevelocity.network.FriendNotifications;
import de.kentoj.scrow.corevelocity.network.SendPlayerWatchdog; import de.kentoj.scrow.corevelocity.network.SendPlayerWatchdog;
import de.kentoj.scrow.corevelocity.network.ServerRegisterWatchdog;
import de.kentoj.scrow.corevelocity.privmsg.LastTargetCache; import de.kentoj.scrow.corevelocity.privmsg.LastTargetCache;
import de.kentoj.scrow.corevelocity.privmsg.MsgCommand; import de.kentoj.scrow.corevelocity.privmsg.MsgCommand;
import de.kentoj.scrow.corevelocity.privmsg.PrivMsgHandler; import de.kentoj.scrow.corevelocity.privmsg.PrivMsgHandler;
import de.kentoj.scrow.corevelocity.privmsg.ReplyCommand; import de.kentoj.scrow.corevelocity.privmsg.ReplyCommand;
import de.kentoj.scrow.corevelocity.serverregistration.ConsulV1ServerInstanceScanner;
import de.kentoj.scrow.corevelocity.serverregistration.RegistrationTask;
import de.kentoj.scrow.corevelocity.serverregistration.ServerInstanceScanner;
import de.kentoj.scrow.velocity.ScrowAPI; import de.kentoj.scrow.velocity.ScrowAPI;
import de.kentoj.scrowlib.convention.ScrowMessageStyle; import de.kentoj.scrowlib.convention.ScrowMessageStyle;
import de.kentoj.scrowlib.utils.EnvUtils;
import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.NamedTextColor;
import java.time.Duration;
@Plugin( @Plugin(
id = "corevelocity", id = "corevelocity",
name = "CoreVelocity", name = "CoreVelocity",
@ -39,11 +43,11 @@ public final class CoreVelocityPlugin {
ScrowAPI.playerCommands().register(new MsgCommand(server, lastTargetCache, privmsgHelper, style).rootLiteral()); ScrowAPI.playerCommands().register(new MsgCommand(server, lastTargetCache, privmsgHelper, style).rootLiteral());
ScrowAPI.commands().register(new OnlineCommand(server).rootLiteral()); ScrowAPI.commands().register(new OnlineCommand(server).rootLiteral());
var registerWatchdog = new ServerRegisterWatchdog(server);
var sendPlayerWatchdog = new SendPlayerWatchdog(server); var sendPlayerWatchdog = new SendPlayerWatchdog(server);
registerWatchdog.listen();
sendPlayerWatchdog.listen(); sendPlayerWatchdog.listen();
ServerInstanceScanner instanceScanner = new ConsulV1ServerInstanceScanner(EnvUtils.envOrThrow("HOST_CONSUL"));
server.getEventManager().register(this, new FriendNotifications()); server.getScheduler().buildTask(this, new RegistrationTask(server, instanceScanner))
.repeat(Duration.ofSeconds(3))
.schedule();
} }
} }

View file

@ -1,23 +0,0 @@
package de.kentoj.scrow.corevelocity.network;
import com.velocitypowered.api.event.Subscribe;
import com.velocitypowered.api.event.connection.DisconnectEvent;
import com.velocitypowered.api.event.connection.PostLoginEvent;
import de.kentoj.scrow.velocity.ScrowAPI;
import de.kentoj.scrowlib.messaging.MessageBroker;
import org.bson.Document;
public class FriendNotifications {
private final MessageBroker messageBroker = ScrowAPI.messageBroker();
@Subscribe
private void onConnect(PostLoginEvent ev) {
messageBroker.publish("event.player.connect", new Document("playerId", ev.getPlayer().getUniqueId()));
}
@Subscribe
private void onDisconnect(DisconnectEvent ev) {
messageBroker.publish("event.player.disconnect", new Document("playerId", ev.getPlayer().getUniqueId()));
}
}

View file

@ -1,38 +0,0 @@
package de.kentoj.scrow.corevelocity.network;
import com.velocitypowered.api.proxy.ProxyServer;
import com.velocitypowered.api.proxy.server.ServerInfo;
import de.kentoj.scrow.velocity.ScrowAPI;
import de.kentoj.scrowlib.messaging.MessageBroker;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.InetSocketAddress;
public class ServerRegisterWatchdog {
private static final Logger log = LoggerFactory.getLogger(ServerRegisterWatchdog.class);
private final MessageBroker messageBroker = ScrowAPI.messageBroker();
private final ProxyServer server;
public ServerRegisterWatchdog(ProxyServer server) {
this.server = server;
}
public void listen() {
messageBroker.subscribe("event.instance.up", doc -> {
var handle = doc.getString("handle");
var port = doc.getInteger("port");
server.unregisterServer(new ServerInfo(handle, new InetSocketAddress(port)));
log.info("registered server {}", handle);
});
messageBroker.subscribe("event.instance.down", doc -> {
var handle = doc.getString("handle");
var port = doc.getInteger("port");
server.unregisterServer(new ServerInfo(handle, new InetSocketAddress(port)));
log.info("unregistered server {}", handle);
});
}
}

View file

@ -0,0 +1,60 @@
package de.kentoj.scrow.corevelocity.serverregistration;
import com.google.common.hash.Hashing;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import com.velocitypowered.api.proxy.server.ServerInfo;
import java.io.Closeable;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.stream.Stream;
public class ConsulV1ServerInstanceScanner implements ServerInstanceScanner, Closeable {
private final String consulUrl;
private final HttpClient client;
public ConsulV1ServerInstanceScanner(String consulUrl) {
this.consulUrl = consulUrl;
this.client = HttpClient.newHttpClient();
}
@Override
// FIXME query cluster-wide
public Stream<ServerInfo> queryMinecraftServers() throws IOException, InterruptedException {
var url = consulUrl + "/v1/agent/services/?filter=" + URLEncoder.encode("\"minecraft\" in Tags", StandardCharsets.UTF_8);
var request = HttpRequest.newBuilder(URI.create(url))
.GET()
.build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
var jsonResponse = JsonParser.parseString(response.body()).getAsJsonObject();
return jsonResponse.entrySet().stream()
.map(Map.Entry::getValue)
.map(JsonElement::getAsJsonObject)
.map(service -> {
var name = toServerName(service.get("Service").getAsString(), service.get("ID").getAsString());
var host = InetSocketAddress.createUnresolved(service.get("Address").getAsString(),
service.get("Port").getAsInt());
return new ServerInfo(name, host);
});
}
private String toServerName(String serviceName, String id) {
return serviceName + "-" + Integer.toUnsignedString(Hashing.murmur3_32_fixed()
.hashString(id, StandardCharsets.UTF_8)
.asInt(), 32);
}
@Override
public void close() throws IOException {
client.close();
}
}

View file

@ -0,0 +1,29 @@
package de.kentoj.scrow.corevelocity.serverregistration;
import com.velocitypowered.api.proxy.ProxyServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
public class RegistrationTask implements Runnable {
private static final Logger log = LoggerFactory.getLogger(RegistrationTask.class);
private final ProxyServer server;
private final ServerInstanceScanner watcher;
public RegistrationTask(ProxyServer server, ServerInstanceScanner watcher) {
this.server = server;
this.watcher = watcher;
}
@Override
public void run() {
try {
watcher.queryMinecraftServers().forEach(server::registerServer);
} catch (IOException | InterruptedException e) {
throw new RuntimeException("Failed querying available servers", e);
}
}
}

View file

@ -0,0 +1,11 @@
package de.kentoj.scrow.corevelocity.serverregistration;
import com.velocitypowered.api.proxy.server.ServerInfo;
import java.io.IOException;
import java.util.stream.Stream;
public interface ServerInstanceScanner {
Stream<ServerInfo> queryMinecraftServers() throws IOException, InterruptedException;
}

37
rules/BUILD Normal file
View file

@ -0,0 +1,37 @@
load("//rules:dev_server.bzl", "dev_server")
load("//rules:fetch_file.bzl", "fetch_file")
dev_server(
name = "dev",
server_jar = ":papermc",
plugins = [
":luckperms",
":viaversion",
":viabackwards",
],
local_plugins = [
"//core/bukkit:plugin",
"//buildserver/configurator:plugin",
],
)
fetch_file(
name = "papermc",
filename = "paper.jar",
url = "https://fill-data.papermc.io/v1/objects/5953a1f1deaa3572be420c3a5b18406c4b752412a78be4acab7c8b467633caed/paper-26.2-100.jar",
)
fetch_file(
name = "luckperms",
filename = "LuckPerms.jar",
url = "https://download.luckperms.net/1652/bukkit/loader/LuckPerms-Bukkit-5.5.65.jar",
)
fetch_file(
name = "viaversion",
filename = "ViaVersion.jar",
url = "https://hangarcdn.papermc.io/plugins/ViaVersion/ViaVersion/versions/5.11.0/PAPER/ViaVersion-5.11.0.jar"
)
fetch_file(
name = "viabackwards",
filename = "ViaBackwards.jar",
url = "https://hangarcdn.papermc.io/plugins/ViaVersion/ViaBackwards/versions/5.11.0/PAPER/ViaBackwards-5.11.0.jar"
)

51
rules/dev_server.bzl Normal file
View file

@ -0,0 +1,51 @@
def _dev_server_impl(ctx):
script = ctx.actions.declare_file(ctx.label.name + "_runner")
content = """
#!/bin/sh
set -eu
cp -f '%s' server.jar
printf eula=true > eula.txt
mkdir -p plugins
rm -f plugins/*.jar
for plugin in %s
do
cp -v \"$plugin\" plugins/
done
mkdir -p postgres-data
podman run \
--rm \
--name scrow-postgres-dev \
-p 5432:5432 \
-v ./postgres-data/:/data \
-e POSTGRES_USER=postgres \
-e POSTGRES_HOST_AUTH_METHOD=trust \
-d \
docker.io/postgres:alpine || true
echo "Starting paper server in $PWD"
exec env HOST_POSTGRES='jdbc:postgresql://127.0.0.1:5432/postgres' java -jar server.jar --nogui %s
""" % (
ctx.file.server_jar.short_path,
" ".join([p.short_path for p in ctx.files.plugins]),
" ".join(["--add-plugin %s" % p.short_path for p in ctx.files.local_plugins]),
)
ctx.actions.write(
output = script,
content = content,
is_executable = True,
)
return DefaultInfo(
executable = script,
runfiles = ctx.runfiles(files = [ctx.file.server_jar] + ctx.files.plugins + ctx.files.local_plugins)
)
dev_server = rule(
implementation = _dev_server_impl,
executable = True,
attrs = {
"server_jar": attr.label(allow_single_file = True, mandatory = True),
"plugins": attr.label_list(),
"local_plugins": attr.label_list(),
},
)

22
rules/fetch_file.bzl Normal file
View file

@ -0,0 +1,22 @@
# TODO implement sha256sum attribute + checksum verification
def _fetch_file_impl(ctx):
out_file = ctx.actions.declare_file(ctx.attr.filename)
ctx.actions.run_shell(
outputs = [out_file],
command = "curl -Lso \"$1\" \"$2\"",
arguments = [out_file.path, ctx.attr.url],
execution_requirements = {
"requires-network": "1",
},
progress_message = "Downloading plugin %s" % ctx.attr.filename,
)
return [DefaultInfo(files = depset([out_file]))]
fetch_file = rule(
implementation = _fetch_file_impl,
attrs = {
"url": attr.string(mandatory = True),
"filename": attr.string(mandatory = True),
},
)

68
rules/publish_plugin.bzl Normal file
View file

@ -0,0 +1,68 @@
# see https://forgejo.org/docs/latest/user/packages/generic/
# We need to delete the previous version to
# FIXME we're leaking credentials in the process listing(args can be seen in ps(1) output)
# solvable by using a temporary .netrc file(create with umask 0600 or smth)
_TEMPLATE = """#!/bin/sh
set -eux
: ${GENERIC_USERNAME:=__USERNAME__}
: ${GENERIC_PASSWORD:=__PASSWORD__}
: ${GENERIC_REPO:=__REPOSITORY__}
URL="$GENERIC_REPO"'/__PACKAGE__/UNVERSIONED/__FILENAME__'
CREDS="$GENERIC_USERNAME:$GENERIC_PASSWORD"
delete_previous_version() {
curl \
--fail \
--user "$CREDS" \
-X DELETE \
"$URL" || true
}
publish_new_version() {
curl \
--fail \
--user "$CREDS" \
--upload-file '__SOURCE__' \
"$URL"
}
echo "Deleting previous version at $URL"
delete_previous_version || echo "Note: 404 is expected if this is the first published version"
echo "Pushing new version to $URL"
publish_new_version
"""
def _publish_plugin_impl(ctx):
script = ctx.actions.declare_file(ctx.label.name + "_publisher")
src = ctx.attr.src[DefaultInfo].files.to_list()[0]
repository = ctx.var.get("generic_repo", "")
username = ctx.var.get("generic_user", "")
password = ctx.var.get("generic_password", "")
ctx.actions.write(
output = script,
is_executable = True,
content = _TEMPLATE
.replace("__USERNAME__", username)
.replace("__PASSWORD__", password)
.replace("__REPOSITORY__", repository)
.replace("__PACKAGE__", ctx.attr.package)
.replace("__SOURCE__", src.short_path)
.replace("__FILENAME__", ctx.attr.filename)
)
return DefaultInfo(
executable = script,
runfiles = ctx.runfiles(files = [src]),
)
publish_plugin = rule(
implementation = _publish_plugin_impl,
executable = True,
attrs = {
"src": attr.label(allow_single_file = True, mandatory = True),
"package": attr.string(mandatory = True),
"filename": attr.string(mandatory = True),
},
)