checker) { this.checker = checker; }
52 |
53 |
54 | public boolean sameCategory(Unitc unit) {
55 | for (UnitCategory c : all) {
56 | if (c.checker.get(unit) && c == this) return true;
57 | }
58 | return false;
59 | }
60 |
61 | public static UnitCategory of(String name) {
62 | for (UnitCategory c : all) {
63 | if (c.name().equals(name)) return c;
64 | }
65 | return null;
66 | }
67 |
68 | /**
69 | * Attempts to dynamically determine the unit's category.
70 | *
71 | * This is unreliable, as a unit can belong to multiple categories.
72 | * Consider using a comparison, with {@link #sameCategory(Unitc)}, instead.
73 | */
74 | public static UnitCategory of(Unitc unit) {
75 | for (UnitCategory c : all) {
76 | if (c.checker.get(unit)) return c;
77 | }
78 | return null; // never happen
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/main/java/fr/zetamap/morecommands/modules/selector/SelectorProperty.java:
--------------------------------------------------------------------------------
1 | /**
2 | * This file is part of MoreCommands. The plugin that adds a bunch of commands to your server.
3 | * Copyright (c) 2025 ZetaMap
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | package fr.zetamap.morecommands.modules.selector;
20 |
21 | import arc.math.geom.Vec2;
22 | import arc.struct.Seq;
23 |
24 | import mindustry.gen.Player;
25 | import mindustry.gen.Unit;
26 |
27 | import fr.zetamap.morecommands.util.StringReader;
28 | import fr.zetamap.morecommands.util.Strings;
29 |
30 |
31 | public abstract class SelectorProperty {
32 | public final String name;
33 | public final Category category;
34 | public final int id;
35 |
36 | /** Uses the class name to determine the property name. Class name without {@code "Property"}, in kebab case. */
37 | public SelectorProperty() { this(Category.other); }
38 | /** Uses the class name to determine the property name. Class name without {@code "Property"}, in kebab case. */
39 | public SelectorProperty(Category category) {
40 | this.name = Strings.kebabize(getClass().getSimpleName().replace("Property", ""));
41 | this.category = category;
42 | this.id = SelectorProperties.add(this);
43 | }
44 |
45 | public SelectorProperty(String name) { this(name, Category.other); }
46 | public SelectorProperty(String name, Category category) {
47 | this.name = name;
48 | this.category = category;
49 | this.id = SelectorProperties.add(this);
50 | }
51 |
52 | public abstract Parsed read(StringReader reader);
53 |
54 |
55 | /** Used to sort properties or override the default selector's behavior. */
56 | public static enum Category {
57 | // Order is important
58 | positioning, bounding, other, sorting, limiting;
59 | }
60 |
61 |
62 | public abstract class Parsed {
63 | public final SelectorProperty property = SelectorProperty.this;
64 |
65 | /** Note that {@code executor} can be {@code null} (e.g. when running from console). */
66 | public abstract boolean passes(Selector selector, Player executor, Vec2 pos, Unit entity);
67 | /**
68 | * Called before or after entities selection, depending on {@link SelectorProperty#category}.
69 | * Note that {@code entities} is an unordered list for optimization purposes, and {@code executor}
70 | * can be {@code null} (e.g. when running from console).
71 | */
72 | public void update(Selector selector, Seq entities, Player executor, Vec2 pos) {}
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/main/java/fr/zetamap/morecommands/modules/selector/properties/TypeProperty.java:
--------------------------------------------------------------------------------
1 | /**
2 | * This file is part of MoreCommands. The plugin that adds a bunch of commands to your server.
3 | * Copyright (c) 2025 ZetaMap
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | package fr.zetamap.morecommands.modules.selector.properties;
20 |
21 | import arc.math.geom.Vec2;
22 | import arc.struct.Seq;
23 |
24 | import mindustry.gen.Player;
25 | import mindustry.gen.Unit;
26 |
27 | import fr.zetamap.morecommands.misc.UnitCategory;
28 | import fr.zetamap.morecommands.modules.selector.Selector;
29 | import fr.zetamap.morecommands.modules.selector.SelectorProperty;
30 | import fr.zetamap.morecommands.util.StringReader;
31 |
32 |
33 | // Doesn't allow empty category because determination is not reliable.
34 | /**
35 | * Accepted formats: {@code type=category}, {@code type=!category},
36 | * {@code type=[category1, ...]} or {@code type=![category1, ...]}.
37 | */
38 | public class TypeProperty extends SelectorProperty {
39 | @Override
40 | public Parsed read(StringReader reader) {
41 | boolean negated = reader.isNegated();
42 | return !reader.isArray() ? new Parsed(readCategory(reader), negated) :
43 | new Parsed(reader.readSet(this::readCategory, "unit category").toSeq(), negated);
44 | }
45 |
46 | public UnitCategory readCategory(StringReader reader) {
47 | String name = reader.readString();
48 | if (name == null) throw reader.expected("a unit category");
49 | UnitCategory category = UnitCategory.of(name = name.toLowerCase());
50 | if (category == null) throw reader.notFound("unit category", name);
51 | return category;
52 | }
53 |
54 |
55 | public class Parsed extends SelectorProperty.Parsed {
56 | /** Not {@code null} if multiple types are specified. */
57 | public final Seq types;
58 | /** {@code null} if multiple types are specified. */
59 | public final UnitCategory type;
60 | public final boolean negated;
61 |
62 | public Parsed(UnitCategory type, boolean negated) {
63 | this.types = null;
64 | this.type = type;
65 | this.negated = negated;
66 | }
67 |
68 | public Parsed(Seq types, boolean negated) {
69 | this.types = types;
70 | this.type = null;
71 | this.negated = negated;
72 | }
73 |
74 | @Override
75 | public boolean passes(Selector selector, Player executor, Vec2 pos, Unit entity) {
76 | // Quite slow because i have no other choice to determine the unit category
77 | return negated ^ (types != null ? types.contains(c -> c.sameCategory(entity)) : type.sameCategory(entity));
78 | }
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/main/java/fr/zetamap/morecommands/modules/selector/properties/DataProperty.java:
--------------------------------------------------------------------------------
1 | /**
2 | * This file is part of MoreCommands. The plugin that adds a bunch of commands to your server.
3 | * Copyright (c) 2025 ZetaMap
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | package fr.zetamap.morecommands.modules.selector.properties;
20 |
21 | import java.lang.reflect.Field;
22 |
23 | import arc.math.geom.Vec2;
24 | import arc.struct.ObjectMap;
25 | import arc.util.serialization.JsonReader;
26 | import arc.util.serialization.JsonValue;
27 |
28 | import mindustry.gen.Player;
29 | import mindustry.gen.Unit;
30 |
31 | import fr.zetamap.morecommands.modules.selector.Selector;
32 | import fr.zetamap.morecommands.modules.selector.SelectorProperty;
33 | import fr.zetamap.morecommands.util.MindustryJson;
34 | import fr.zetamap.morecommands.util.StringReader;
35 |
36 |
37 | /** minecraft's nbt like property. WIP */
38 | public class DataProperty extends SelectorProperty {
39 | private final JsonReader reader = new JsonReader();
40 |
41 | @Override
42 | public Parsed read(StringReader reader) {
43 | boolean negated = reader.isNegated();
44 | if (reader.peekNext() != '{') throw reader.expected("a map start", reader.peekNext());
45 | // TODO: Find a way to read json without error about leading comma or brace
46 | try { return new Parsed(this.reader.parse(reader.readRemaining()), negated); }
47 | catch (Exception e) { throw reader.error(e.getMessage()); }
48 | }
49 |
50 |
51 | public class Parsed extends SelectorProperty.Parsed {
52 | public final JsonValue data;
53 | public final boolean negated;
54 | protected Class extends Unit> readedType;
55 | protected ObjectMap readed;
56 |
57 | public Parsed(JsonValue data, boolean negated) {
58 | this.data = data;
59 | this.negated = negated;
60 | // Should be an object
61 | if (!data.isObject()) throw new IllegalArgumentException("Should be a json object");
62 | }
63 |
64 | @Override
65 | public boolean passes(Selector selector, Player executor, Vec2 pos, Unit entity) {
66 | // Try to cache the result, better than nothing ¯\_(ツ)_/¯
67 | if (entity.getClass() != readedType) {
68 | readed = MindustryJson.get().readFields(entity.getClass(), data);
69 | readedType = entity.getClass();
70 | }
71 |
72 | // This is really inefficient (can hold lot of memory) but this is the only way
73 | for (ObjectMap.Entry e : readed) {
74 | try {
75 | if (!e.key.get(entity).equals(e.value)) return negated;
76 | } catch (Exception ignored) {}
77 | }
78 | return !negated;
79 | }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/main/java/fr/zetamap/morecommands/modules/selector/properties/HasitemProperty.java:
--------------------------------------------------------------------------------
1 | /**
2 | * This file is part of MoreCommands. The plugin that adds a bunch of commands to your server.
3 | * Copyright (c) 2025 ZetaMap
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 |
20 | package fr.zetamap.morecommands.modules.selector.properties;
21 |
22 | import arc.math.geom.Vec2;
23 | import arc.struct.ObjectMap;
24 |
25 | import mindustry.Vars;
26 | import mindustry.gen.Player;
27 | import mindustry.gen.Unit;
28 | import mindustry.type.Item;
29 |
30 | import fr.zetamap.morecommands.misc.Range;
31 | import fr.zetamap.morecommands.misc.RangeParser;
32 | import fr.zetamap.morecommands.modules.selector.Selector;
33 | import fr.zetamap.morecommands.modules.selector.SelectorProperty;
34 | import fr.zetamap.morecommands.util.StringReader;
35 |
36 |
37 | /** Accepted formats: {@code hasitem=[item1, !item2, item2=quantity, !item3=..max, item4=min.., item5=min..max]} */
38 | public class HasitemProperty extends SelectorProperty {
39 | @Override
40 | public Parsed read(StringReader reader) {
41 | reader.skipWhitespaces();
42 | if (!reader.isArray()) throw reader.expected("an array");
43 | ObjectMap- > items = new ObjectMap<>();
44 | ObjectMap
- negated = new ObjectMap<>();
45 | reader.readArray(r -> {
46 | boolean negate = r.isNegated();
47 | String name = r.readString();
48 | if (name == null || name.isEmpty()) throw r.expected("an item name");
49 | Item item = Vars.content.item(name);
50 | if (item == null) throw r.notFound("item", name);
51 | Range range = null;
52 | if (r.peek() == '=') {
53 | r.skip();
54 | range = RangeParser.parseInt(reader, false);
55 | }
56 | items.put(item, range);
57 | negated.put(item, negate);
58 | });
59 | return new Parsed(items, negated);
60 | }
61 |
62 |
63 | public class Parsed extends SelectorProperty.Parsed {
64 | public final ObjectMap
- > items;
65 | public final ObjectMap
- negated;
66 |
67 | public Parsed(ObjectMap
- > items, ObjectMap
- negated) {
68 | this.items = items;
69 | this.negated = negated;
70 | }
71 |
72 | @Override
73 | public boolean passes(Selector selector, Player executor, Vec2 pos, Unit entity) {
74 | if (!entity.hasItem() || !items.containsKey(entity.item())) return false;
75 | Range range = items.get(entity.item());
76 | Boolean negate = negated.get(entity.item());
77 | return (range == null || range.contains(entity.stack().amount)) ^ (negate != null && negate);
78 | }
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/main/java/fr/zetamap/morecommands/misc/RangeParser.java:
--------------------------------------------------------------------------------
1 | /**
2 | * This file is part of MoreCommands. The plugin that adds a bunch of commands to your server.
3 | * Copyright (c) 2025 ZetaMap
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 |
20 | package fr.zetamap.morecommands.misc;
21 |
22 | import fr.zetamap.morecommands.util.StringReader;
23 |
24 |
25 | public class RangeParser {
26 | public static Range parseInt(String input, boolean notNegative) throws StringReader.ParseException {
27 | return parseInt(new StringReader(input), notNegative);
28 | }
29 | public static Range parseInt(StringReader reader, boolean notNegative) throws StringReader.ParseException {
30 | reader.skipWhitespaces();
31 | if (!reader.canPeekNext()) throw reader.expected("an int or a range");
32 | Integer min = reader.readNumeric(notNegative, false);
33 | if (!reader.canPeekNext() || reader.peekNext() != '.') {
34 | if (min == null) throw reader.expected("an int or a range");
35 | return Range.fixed(min);
36 | }
37 | if (reader.peekNext(1) != '.') throw reader.error("Invalid int range");
38 | reader.skip(2);
39 | Integer max = reader.readNumeric(notNegative, false);
40 | if (min == null && max == null) throw reader.error("Invalid int range");
41 | if (min != null && max != null && min > max) throw reader.error("Min range greater than max");
42 | return Range.of(min, max);
43 | }
44 |
45 | public static Range parseFloat(String input, boolean notNegative) throws StringReader.ParseException {
46 | return parseFloat(new StringReader(input), notNegative);
47 | }
48 | public static Range parseFloat(StringReader reader, boolean notNegative) throws StringReader.ParseException {
49 | reader.skipWhitespaces();
50 | if (!reader.canPeekNext()) throw reader.expected("a float or a range");
51 | Float min = reader.readDecimal(notNegative, false);
52 |
53 | // The first dot can be consumed by the first decimal read
54 | if (reader.peekNext(-1) == '.') reader.skip(-1);
55 | if (!reader.canPeekNext() || reader.peekNext() != '.') {
56 | if (min == null) throw reader.expected("a float or a range");
57 | return Range.fixed(min);
58 | }
59 | if (reader.peekNext(1) != '.') throw reader.error("Invalid float range");
60 | reader.skip(2);
61 | if (reader.peekNext() == '.') throw reader.error("Invalid float range");
62 |
63 | Float max = reader.readDecimal(notNegative, false);
64 | if (min == null && max == null) throw reader.error("Invalid float range");
65 | if (min != null && max != null && min > max) throw reader.error("Min range greater than max");
66 | return Range.of(min, max);
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/main/java/fr/zetamap/morecommands/modules/security/AdminUsidModule.java:
--------------------------------------------------------------------------------
1 | /**
2 | * This file is part of MoreCommands. The plugin that adds a bunch of commands to your server.
3 | * Copyright (c) 2025 ZetaMap
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | package fr.zetamap.morecommands.modules.security;
20 |
21 | import arc.struct.ObjectMap;
22 | import arc.struct.ObjectSet;
23 | import arc.struct.Seq;
24 |
25 | import mindustry.Vars;
26 | import mindustry.net.Administration;
27 |
28 | import fr.zetamap.morecommands.module.AbstractSaveableModule;
29 | import fr.zetamap.morecommands.util.JsonSettings;
30 |
31 |
32 | /** Modify some {@link Administration} methods to handle multiple usid. */
33 | public class AdminUsidModule extends AbstractSaveableModule {
34 | /** Direct access to the modified {@link Administration}. */
35 | public WrappedAdministration admins;
36 |
37 | @Override
38 | protected void initImpl() {
39 | Vars.netServer.admins.forceSave(); // in case of
40 | Vars.netServer.admins = admins = new WrappedAdministration(Vars.netServer.admins);
41 | }
42 |
43 | @SuppressWarnings("unchecked")
44 | @Override
45 | protected void loadImpl(JsonSettings settings) {
46 | if (admins == null) return;
47 |
48 | admins.usids.clear();
49 | for (String uuid : settings.keys())
50 | admins.usids.put(uuid, settings.getOrPut(uuid, ObjectSet.class, String.class, ObjectSet::new));
51 |
52 | // Add missing admin usids
53 | admins.getAdmins().each(p -> admins.usids.get(p.id, ObjectSet::new).add(p.adminUsid));
54 | }
55 |
56 | @Override
57 | protected void saveImpl(JsonSettings settings) {
58 | if (admins == null) return;
59 |
60 | admins.usids.each((uuid, usids) -> settings.put(uuid, String.class, usids.toSeq()));
61 | }
62 |
63 |
64 | public class WrappedAdministration extends Administration {
65 | public final ObjectMap> usids = new ObjectMap<>();
66 |
67 | public WrappedAdministration(Administration old) {
68 | // Copy callbacks
69 | chatFilters.set(old.chatFilters);
70 | actionFilters.set(old.actionFilters);
71 | }
72 |
73 | public Seq getUsids(String id) {
74 | return usids.get(id).toSeq();
75 | }
76 |
77 | @Override
78 | public boolean adminPlayer(String id, String usid) {
79 | usids.get(id, ObjectSet::new).add(usid);
80 | setModified();
81 | return super.adminPlayer(id, usid);
82 | }
83 |
84 | @Override
85 | public boolean unAdminPlayer(String id) {
86 | usids.remove(id);
87 | setModified();
88 | return super.unAdminPlayer(id);
89 | }
90 |
91 | @Override
92 | public boolean isAdmin(String id, String usid) {
93 | ObjectSet u;
94 | return super.isAdmin(id, usid) || (u = usids.get(id)) != null && u.contains(usid);
95 | }
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/src/main/java/fr/zetamap/morecommands/modules/selector/SelectorModule.java:
--------------------------------------------------------------------------------
1 | /**
2 | * This file is part of MoreCommands. The plugin that adds a bunch of commands to your server.
3 | * Copyright (c) 2025 ZetaMap
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | package fr.zetamap.morecommands.modules.selector;
20 |
21 | import fr.zetamap.morecommands.PlayerData;
22 | import fr.zetamap.morecommands.misc.CoordinatesParser;
23 | import fr.zetamap.morecommands.misc.Gatekeeper;
24 | import fr.zetamap.morecommands.misc.Players;
25 | import fr.zetamap.morecommands.module.AbstractModule;
26 | import fr.zetamap.morecommands.module.ModuleFactory;
27 |
28 |
29 | /**
30 | * Minecraft-like selectors.
31 | * Nicknames must not start with {@code '@'} or {@code '~'} to avoid ambiguity with selector and coordinate prefixes.
32 | *
33 | * To understand more about selectors and properties, please read: https://minecraft.wiki/w/Target_selectors
34 | */
35 | public class SelectorModule extends AbstractModule {
36 | public boolean enabled() {
37 | return ModuleFactory.enabled(this);
38 | }
39 |
40 | public void enable() {
41 | ModuleFactory.enable(this);
42 | }
43 |
44 | public void disable() {
45 | ModuleFactory.disable(this);
46 | }
47 |
48 | public SelectorParser parse(PlayerData executor, String[] args) {
49 | return parse(executor, args, 0, args.length, false);
50 | }
51 | public SelectorParser parse(PlayerData executor, String[] args, boolean onlyPlayers) {
52 | return parse(executor, args, 0, args.length, onlyPlayers);
53 | }
54 | public SelectorParser parse(PlayerData executor, String[] args, int from, int to) {
55 | return parse(executor, args, from, to, false);
56 | }
57 | /** This method will notify the user if selectors are disabled. */
58 | public SelectorParser parse(PlayerData executor, String[] args, int from, int to, boolean onlyPlayers) {
59 | if (args.length == 0 || from < 0 || to > args.length || from >= to || args[from].isEmpty()) {
60 | Players.err(executor, "Missing player name/uuid or selector.");
61 | return null;
62 | } else if (Selectors.isSelector(args[from]) && !enabled()) {
63 | Players.err(executor, "Selectors are disabled, you cannot use them.");
64 | return null;
65 | }
66 | return SelectorParser.parse(executor, args, from, to, onlyPlayers);
67 | }
68 |
69 | @Override
70 | protected void initImpl() {
71 | Selectors.init();
72 | SelectorProperties.init();
73 |
74 | Gatekeeper.add(internalName(), ctx ->
75 | !ctx.strippedName.isEmpty() && (ctx.strippedName.charAt(0) == Selectors.prefix ||
76 | ctx.strippedName.charAt(0) == CoordinatesParser.worldRelativePrefix) ?
77 | Gatekeeper.reject("Your nickname cannot start with '[orange]" + ctx.strippedName.charAt(0) + "[]'.") :
78 | Gatekeeper.accept());
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/main/java/fr/zetamap/morecommands/modules/selector/properties/UnitProperty.java:
--------------------------------------------------------------------------------
1 | /**
2 | * This file is part of MoreCommands. The plugin that adds a bunch of commands to your server.
3 | * Copyright (c) 2025 ZetaMap
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | package fr.zetamap.morecommands.modules.selector.properties;
20 |
21 | import arc.math.geom.Vec2;
22 | import arc.struct.ObjectSet;
23 |
24 | import mindustry.Vars;
25 | import mindustry.gen.Player;
26 | import mindustry.gen.Unit;
27 | import mindustry.type.UnitType;
28 |
29 | import fr.zetamap.morecommands.modules.selector.Selector;
30 | import fr.zetamap.morecommands.modules.selector.SelectorProperty;
31 | import fr.zetamap.morecommands.util.StringReader;
32 |
33 |
34 | /**
35 | * Accepted formats: {@code unit=}, {@code unit=!}, {@code unit=unit}, {@code unit=!unit},
36 | * {@code unit=[unit1, ...]} or {@code unit=![unit1, ...]}.
37 | */
38 | public class UnitProperty extends SelectorProperty {
39 | @Override
40 | public Parsed read(StringReader reader) {
41 | boolean negated = reader.isNegated();
42 | return !reader.isArray() ? new Parsed(readUnit(reader, true), negated) :
43 | new Parsed(reader.readSet(this::readUnit, "unit name"), negated);
44 | }
45 |
46 | public UnitType readUnit(StringReader reader) { return readUnit(reader, false); }
47 | public UnitType readUnit(StringReader reader, boolean optional) {
48 | String name = reader.readString();
49 | if (name == null) {
50 | if (optional) return null;
51 | throw reader.expected("a unit name");
52 | }
53 | UnitType unit = Vars.content.unit(name);
54 | if (unit == null && !optional) throw reader.notFound("unit", name);
55 | return unit;
56 | }
57 |
58 |
59 | public class Parsed extends SelectorProperty.Parsed {
60 | /** Not {@code null} if multiple units are specified. */
61 | public final ObjectSet units;
62 | /** {@code null} if no or multiple units are specified. */
63 | public final UnitType unit;
64 | public final boolean negated;
65 |
66 | public Parsed(UnitType unit, boolean negated) {
67 | this.units = null;
68 | this.unit = unit;
69 | this.negated = negated;
70 | }
71 |
72 | public Parsed(ObjectSet units, boolean negated) {
73 | this.units = units;
74 | this.unit = null;
75 | this.negated = negated;
76 | }
77 |
78 | @Override
79 | public boolean passes(Selector selector, Player executor, Vec2 pos, Unit entity) {
80 | return negated ^ (units != null ? units.contains(entity.type) :
81 | unit != null ? unit == entity.type :
82 | // always false if the player is dead or if it's the console
83 | executor == null || executor.dead() ? negated :
84 | executor.unit().type == entity.type);
85 | }
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/src/main/java/fr/zetamap/morecommands/modules/server/PreConnect.java:
--------------------------------------------------------------------------------
1 | /**
2 | * This file is part of MoreCommands. The plugin that adds a bunch of commands to your server.
3 | * Copyright (c) 2025 ZetaMap
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | package fr.zetamap.morecommands.modules.server;
20 |
21 | import mindustry.core.Version;
22 |
23 |
24 | /** Try to anticipate a refused connection by using the last server discovery information. */
25 | public class PreConnect {
26 | /** Check for connection availability using {@link Server#info} provided by the last {@link Server#ping()} call. */
27 | public static Availability verify(boolean adminPlayer, Server server) {
28 | return
29 | server.info == null ? Availability.serverClosed :
30 | !adminPlayer && server.adminOnly ? Availability.adminOnly :
31 | server.info.playerLimit > 0 && server.info.players >= server.info.playerLimit && !adminPlayer ? Availability.playerLimit :
32 | !server.info.versionType.equals(Version.type) ? Availability.typeMismatch :
33 | Version.build == -1 && server.info.version != -1 ? Availability.customClient :
34 | server.info.version != Version.build && Version.build != -1 && server.info.version != -1 ?
35 | Version.build > server.info.version ? Availability.serverOutdated : Availability.clientOutdated :
36 | Availability.ok;
37 | }
38 |
39 |
40 | /** A part come from {@link mindustry.net.Packets.KickReason}. */
41 | public static enum Availability {
42 | ok, serverOutdated, clientOutdated, typeMismatch, customClient, playerLimit, adminOnly, serverClosed;
43 |
44 | public boolean ok() {
45 | return this == ok;
46 | }
47 |
48 | public String toReason(Server server) {
49 | switch (this) {
50 | case ok: return "[green]Connection success.";
51 | case adminOnly: return "This server is only for admins.";
52 | case serverOutdated: return "The server is in a lower version of Mindustry. \n"
53 | + "[gray]Current: [lightgray]v" + Version.build + "[], Required: [lightgray]v"
54 | + server.info.version;
55 | case clientOutdated: return "The server is in a newer version of Mindustry. \n"
56 | + "[gray]Current: [lightgray]v" + Version.build + "[], Required: [lightgray]v"
57 | + server.info.version;
58 | case typeMismatch: return "The server is not compatible with this Mindustry version. \n"
59 | + "[gray]Current: [lightgray]" + Version.type + "[], Required: [lightgray]"
60 | + server.info.versionType;
61 | case customClient: return "The server does not accept custom Mindustry versions.";
62 | case playerLimit: return "Server full. [gray]([lightgray]" + server.info.players + "[]/[lightgray]"
63 | + server.info.playerLimit + "[])";
64 | case serverClosed: return "The server is not responding. [gray]([lightgray]timeout[])";
65 | default: return "[lightgray][]";
66 | }
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/main/java/fr/zetamap/morecommands/modules/help/HelpModule.java:
--------------------------------------------------------------------------------
1 | /**
2 | * This file is part of MoreCommands. The plugin that adds a bunch of commands to your server.
3 | * Copyright (c) 2025 ZetaMap
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | package fr.zetamap.morecommands.modules.help;
20 |
21 | import arc.math.Mathf;
22 | import arc.struct.Seq;
23 | import arc.util.CommandHandler.Command;
24 |
25 | import fr.zetamap.morecommands.command.*;
26 | import fr.zetamap.morecommands.misc.Players;
27 | import fr.zetamap.morecommands.module.AbstractModule;
28 | import fr.zetamap.morecommands.util.Strings;
29 |
30 |
31 | public class HelpModule extends AbstractModule {
32 | @Override
33 | public void registerServerCommands(ServerCommandHandler handler) {
34 |
35 | }
36 |
37 | @Override
38 | public void registerClientCommands(ClientCommandHandler handler) {
39 | handler.add("help", "[command|page|selectors]", "Lists all commands.", (args, player) -> {
40 | String command;
41 | int perPage = 8, page = 1, pages;
42 |
43 | if (args.length == 1) {
44 | if (args[0].equals("selectors")) {
45 | mindustry.gen.Call.openURI(player.player.con,
46 | "https://github.com/ZetaMap/MoreCommands/blob/main/README.md#selectors");
47 | return;
48 | }
49 |
50 | page = Strings.parseInt(args[0]);
51 |
52 | if (page == Integer.MIN_VALUE) command = args[0];
53 | else command = null;
54 | } else command = null;
55 |
56 | if (command != null) {
57 | Command c = handler.handler.getCommandList().find(cc -> cc.text.equals(command));
58 |
59 | // Act like the command is not found when the player is not an administrator
60 | if (c == null || (!player.admin() && handler.isAdmin(c.text)))
61 | Players.err(player, "No command named '[orange]@[]' found.", command);
62 | else Players.info(player, "[orange]@@[white] @ [lightgray]- @", handler.handler.prefix, c.text, c.paramText,
63 | c.description);
64 | return;
65 | }
66 |
67 | Seq commands = handler.handler.getCommandList();
68 | if (!player.admin()) commands = commands.select(c -> !handler.isAdmin(c.text));
69 | pages = Mathf.ceil((float)commands.size / perPage);
70 |
71 | if (page < 1 || page > pages) {
72 | Players.err(player, "'[orange]page[]' must be a number between [orange]1[] and [orange]@[].", pages);
73 | return;
74 | }
75 |
76 | Players.info(player, "[orange]-- Commands page [lightgray]@[gray]/[]@ [gray]([]@[gray])[][] --", page, pages,
77 | commands.size);
78 | StringBuilder builder = new StringBuilder();
79 | for(int i=perPage*(page-1); i.
17 | */
18 |
19 | package fr.zetamap.morecommands.modules.selector.properties;
20 |
21 | import arc.math.geom.Vec2;
22 | import arc.struct.ObjectSet;
23 | import arc.util.Structs;
24 |
25 | import mindustry.game.Team;
26 | import mindustry.gen.Player;
27 | import mindustry.gen.Unit;
28 |
29 | import fr.zetamap.morecommands.modules.selector.Selector;
30 | import fr.zetamap.morecommands.modules.selector.SelectorProperty;
31 | import fr.zetamap.morecommands.util.StringReader;
32 |
33 |
34 | /**
35 | * Accepted formats: {@code team=}, {@code team=!}, {@code team=team}, {@code team=!team},
36 | * {@code team=[team1, ...]} or {@code team=![team1, ...]}.
37 | * {@code team} value can be a named team from {@link Team#baseTeams} (e.g. {@code sharded}, {@code crux}),
38 | * an unnamed team from {@link Team#all} {@code team#7-255} (e.g. {@code team#7}, {@code team#46})
39 | * or a team id from {@link Team#all} {@code 0 -> 255} (e.g. {@code #1}, {@code #64})
40 | */
41 | public class TeamProperty extends SelectorProperty {
42 | @Override
43 | public Parsed read(StringReader reader) {
44 | boolean negated = reader.isNegated();
45 | return !reader.isArray() ? new Parsed(readTeam(reader, true), negated) :
46 | new Parsed(reader.readSet(this::readTeam, "team name"), negated);
47 | }
48 |
49 | public Team readTeam(StringReader reader) { return readTeam(reader, false); }
50 | public Team readTeam(StringReader reader, boolean optional) {
51 | if (reader.peekNext() == '#') {
52 | reader.readNext();
53 | int id = reader.readNumeric(true);
54 | if (id < 0 || id > Team.all.length-1) throw reader.error("Invalid team id (range 0-255)");
55 | return Team.get(id);
56 | }
57 | String name = reader.readString();
58 | if (name == null) {
59 | if (optional) return null;
60 | throw reader.expected("a team name");
61 | }
62 | Team team = Structs.find(Team.all, t -> t.name.equals(name));
63 | if (team == null && !optional) throw reader.notFound("team", name);
64 | return team;
65 | }
66 |
67 |
68 | public class Parsed extends SelectorProperty.Parsed {
69 | /** Not {@code null} if multiple teams are specified. */
70 | public final ObjectSet teams;
71 | /** {@code null} if no or multiple teams are specified. */
72 | public final Team team;
73 | public final boolean negated;
74 |
75 | public Parsed(Team team, boolean negated) {
76 | this.teams = null;
77 | this.team = team;
78 | this.negated = negated;
79 | }
80 |
81 | public Parsed(ObjectSet teams, boolean negated) {
82 | this.teams = teams;
83 | this.team = null;
84 | this.negated = negated;
85 | }
86 |
87 | @Override
88 | public boolean passes(Selector selector, Player executor, Vec2 pos, Unit entity) {
89 | return negated ^ (teams != null ? teams.contains(entity.team) :
90 | team != null ? team == entity.team :
91 | executor == null || executor.team() == entity.team);
92 | }
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/main/java/fr/zetamap/morecommands/modules/selector/Selectors.java:
--------------------------------------------------------------------------------
1 | /**
2 | * This file is part of MoreCommands. The plugin that adds a bunch of commands to your server.
3 | * Copyright (c) 2021-2025 ZetaMap
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | package fr.zetamap.morecommands.modules.selector;
20 |
21 | import arc.func.Cons;
22 | import arc.struct.ObjectMap;
23 | import arc.struct.Seq;
24 |
25 | import mindustry.gen.Groups;
26 |
27 |
28 | public class Selectors {
29 | public static final char prefix = '@';
30 | private static final ObjectMap all = new ObjectMap<>();
31 |
32 | /** Default selectors. */
33 | public static Selector
34 | // Player related
35 | nearestPlayer, randomPlayer, allPlayers, teamPlayers, self,
36 | // Unit related
37 | nearestUnit, randomUnit, allUnits, teamUnits,
38 | // Both related
39 | nearestEntity, randomEntity, allEntities, teamEntities;
40 |
41 |
42 | public static void init() {
43 | nearestPlayer = new Selector("p", 1, Sorting.nearest, Selector.playerExtractor);
44 | randomPlayer = new Selector("r", 1, Sorting.random, Selector.playerExtractor);
45 | allPlayers = new Selector("a", Selector.playerExtractor);
46 | teamPlayers = new Selector("t", (l, e) -> Groups.player.each(p -> e == null || p.team() == e.team(), p -> l.add(p.unit())));
47 | self = new Selector("s", 1, null, (l, e) -> {
48 | if (e == null) throw new IllegalArgumentException("Unavailable selector");
49 | l.add(e.unit());
50 | });
51 |
52 | nearestUnit = new Selector("nu", 1, Sorting.nearest, Selector.unitExtractor);
53 | randomUnit = new Selector("ru", 1, Sorting.random, Selector.unitExtractor);
54 | allUnits = new Selector("u", Selector.unitExtractor);
55 | teamUnits = new Selector("tu", (l, e) -> Groups.unit.each(p -> !p.isPlayer() && e == null || p.team() == e.team(), l::add));
56 |
57 | nearestEntity = new Selector("n", 1, Sorting.nearest, Selector.entityExtractor);
58 | randomEntity = new Selector("re", 1, Sorting.random, Selector.entityExtractor);
59 | allEntities = new Selector("e", Selector.entityExtractor);
60 | teamEntities = new Selector("te", (l, e) -> Groups.unit.each(p -> e == null || p.team() == e.team(), l::add));
61 | }
62 |
63 | public static Selector get(String name) {
64 | return name.isEmpty() ? null : all.get(name.charAt(0) == prefix ? name.substring(1) : name);
65 | }
66 |
67 | public static boolean isSelector(String arg) {
68 | return !arg.isEmpty() && arg.charAt(0) == prefix;
69 | }
70 |
71 | /** @throws IllegalArgumentException if another selector is registered with the same name. */
72 | public static int add(Selector selector) {
73 | if (all.containsKey(selector.name))
74 | throw new IllegalArgumentException("another selector is named '"+selector.name+"'");
75 | // Validate the name depending on what StringReader can accept
76 | if (fr.zetamap.morecommands.util.Strings.contains(selector.name, c ->
77 | !fr.zetamap.morecommands.util.StringReader.isAlpha(c)))
78 | throw new IllegalArgumentException("invalid selector name");
79 | all.put(selector.name, selector);
80 | return all.size-1;
81 | }
82 |
83 | public static void each(Cons consumer) {
84 | all.each((n, s) -> consumer.get(s));
85 | }
86 |
87 | public static Seq all() {
88 | return all.values().toSeq();
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/src/main/java/fr/zetamap/morecommands/modules/security/Punishment.java:
--------------------------------------------------------------------------------
1 | /**
2 | * This file is part of MoreCommands. The plugin that adds a bunch of commands to your server.
3 | * Copyright (c) 2021-2025 ZetaMap
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | package fr.zetamap.morecommands.modules.security;
20 |
21 | import arc.util.Nullable;
22 | import arc.util.Time;
23 |
24 |
25 | public class Punishment {
26 | static int lastId;
27 |
28 | public final int id;
29 | public @Nullable String author;
30 | public final String target;
31 | public @Nullable String address;
32 | public final Type type;
33 | public final long creation;
34 | public long expire;
35 | public @Nullable String reason;
36 | public @Nullable Pardon pardon;
37 |
38 | Punishment(int id, String author, String target, String address, Type type, long creation, long expire, String reason,
39 | Pardon pardon) {
40 | this.id = id;
41 | this.author = author;
42 | this.target = target;
43 | this.address = address;
44 | this.type = type;
45 | this.creation = creation;
46 | this.expire = Math.max(expire, -1);
47 | this.reason = reason;
48 | this.pardon = pardon;
49 | }
50 |
51 | /** A negative {@code duration} can be used to specify a forever punishment. */
52 | public Punishment(String author, String target, String address, Type type, long duration, String reason) {
53 | this.id = lastId++;
54 | this.author = author;
55 | this.target = target;
56 | this.address = address;
57 | this.type = type;
58 | this.creation = Time.millis();
59 | this.expire = duration < 0 ? -1 : creation + duration;
60 | this.reason = reason;
61 | }
62 |
63 | public boolean expired() {
64 | return pardoned() || !permanant() && Time.millis() > expire;
65 | }
66 |
67 | public boolean permanant() {
68 | return expire < 0;
69 | }
70 |
71 | public boolean pardoned() {
72 | return pardon != null;
73 | }
74 |
75 | public long duration() {
76 | return expire - creation;
77 | }
78 |
79 | public long remaining() {
80 | return permanant() ? Long.MAX_VALUE : Time.millis() - expire;
81 | }
82 |
83 | public void setDuration(long duration) {
84 | expire = creation + duration;
85 | }
86 |
87 |
88 | public static enum Type {
89 | ban("banned", true, PunishmentDuration.oneMonth),
90 | kick("kicked", true, PunishmentDuration.thirtyMinutes),
91 | votekick("vote kicked", true, PunishmentDuration.oneHour),
92 | warn("warned", false, PunishmentDuration.permanant),
93 | mute("muted", false, PunishmentDuration.threeHours),
94 | freeze("frozen", false, PunishmentDuration.oneHour);
95 |
96 | public static final Type[] all = values();
97 |
98 | public final String verb;
99 | /** Whether this kind of punishment implies a kick of the target. */
100 | public final boolean impliesKick;
101 | public final PunishmentDuration defaultDuration;
102 |
103 | Type(String verb, boolean impliesKick, PunishmentDuration defaultDuration) {
104 | this.verb = verb;
105 | this.impliesKick = impliesKick;
106 | this.defaultDuration = defaultDuration;
107 | }
108 | }
109 |
110 | public static class Pardon {
111 | public @Nullable String author;
112 | public final long when;
113 | public @Nullable String reason;
114 |
115 | public Pardon() { this(null, null); }
116 | public Pardon(String author) { this(author, null); }
117 | public Pardon(String author, String reason) { this(author, Time.millis(), reason); }
118 | public Pardon(String author, long when, String reason) {
119 | this.author = author;
120 | this.when = when;
121 | this.reason = reason;
122 | }
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/src/main/java/fr/zetamap/morecommands/Main.java:
--------------------------------------------------------------------------------
1 | /**
2 | * This file is part of MoreCommands. The plugin that adds a bunch of commands to your server.
3 | * Copyright (c) 2021-2025 ZetaMap
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | package fr.zetamap.morecommands;
20 |
21 | import arc.util.CommandHandler;
22 | import arc.util.Reflect;
23 | import arc.util.Time;
24 |
25 | import mindustry.Vars;
26 | import mindustry.mod.Mods;
27 | import mindustry.net.Administration.Config;
28 |
29 | import fr.zetamap.morecommands.migration.SettingsMigrator;
30 | import fr.zetamap.morecommands.misc.Gatekeeper;
31 | import fr.zetamap.morecommands.module.ModuleFactory;
32 | import fr.zetamap.morecommands.util.Autosaver;
33 | import fr.zetamap.morecommands.util.Logger;
34 | import fr.zetamap.morecommands.util.VersionChecker;
35 |
36 |
37 | public class Main extends mindustry.mod.Plugin {
38 | private static final Logger logger = new Logger(true);
39 |
40 | {
41 | ModuleFactory.configFile = getConfig();
42 | ModuleFactory.preInit();
43 | Modules.register(this);
44 | }
45 |
46 | @Override
47 | public void init() {
48 | // TODO: should start after the server, because of possible added modules
49 | long time = Time.nanos();
50 |
51 | Logger.init(this);
52 | logger.info("&lc>>>&fr MoreCommands plugin is loading...\n");
53 |
54 | VersionChecker.checkFor(this);
55 |
56 | PlayerData.init();
57 | Gatekeeper.init();
58 | if (check(ModuleFactory::init)) return;
59 | SettingsMigrator.enabled = false; //TODO: debug
60 | if (check(SettingsMigrator::migrateAllTheShittySettingsIMade)) return;
61 |
62 | Autosaver.spacing(Config.autosaveSpacing.num());
63 | Autosaver.start();
64 | Autosaver.save(); // just to be sure
65 |
66 | // Use reflection to follow the server autosave spacing
67 | Reflect.set(Config.autosaveSpacing, "changed", (Runnable)() -> Autosaver.spacing(Config.autosaveSpacing.num()));
68 |
69 | logger.info("\n&lc>>>&fr MoreCommands plugin loaded in @ seconds! enjoy the fun =)",
70 | Time.timeSinceNanos(time) / 1_000_000_000f);
71 | }
72 |
73 | @Override
74 | public void registerServerCommands(CommandHandler handler) {
75 | ModuleFactory.registerServerCommands(handler);
76 | }
77 |
78 | @Override
79 | public void registerClientCommands(CommandHandler handler) {
80 | ModuleFactory.registerClientCommands(handler);
81 | }
82 |
83 | private static Mods.ModMeta meta;
84 | public static Mods.ModMeta getMeta() {
85 | if (meta != null) return meta;
86 | Mods.LoadedMod load = Vars.mods.getMod(Main.class);
87 | if(load == null) throw new IllegalArgumentException("Mod is not loaded yet (or missing)!");
88 | return meta = load.meta;
89 | }
90 |
91 | private static boolean check(arc.util.UnsafeRunnable run) {
92 | try {
93 | run.run();
94 | return false;
95 | } catch (Throwable t) {
96 | logger.err(t);
97 | error();
98 | return true;
99 | }
100 | }
101 |
102 | private static boolean check(arc.func.Boolp run) {
103 | try {
104 | if (run.get()) return false;
105 | } catch (Throwable t) { logger.err(t); }
106 | error();
107 | return true;
108 | }
109 |
110 | private static void error() {
111 | logger.err("\n########################################\n\n"
112 | + "&lc>>>&fr MoreCommands plugin failed to initialize due to previous error(s)!\n"
113 | + "&lc>>>&fr Most often, this is caused by an invalid or corrupted configuration files.\n"
114 | + "&lc>>>&fr If this is not the case and this/these error(s) are recurring, please report them here:\n"
115 | + "&lc>>>&fr @", "https://github.com/ZetaMap/MoreCommands/issues/new");
116 | ModuleFactory.setError();
117 | ModuleFactory.dispose(); // "Uninitialize" MoreCommands. At least, removes the commands and stops the automated things.
118 | logger.err("&lc>>>&fr\n"
119 | + "&lc>>>&fr MoreCommands has been uninitialized to avoid further issues.\n"
120 | + "\n########################################\n");
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/src/main/java/fr/zetamap/morecommands/Modules.java:
--------------------------------------------------------------------------------
1 | /**
2 | * This file is part of MoreCommands. The plugin that adds a bunch of commands to your server.
3 | * Copyright (c) 2025 ZetaMap
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | package fr.zetamap.morecommands;
20 |
21 | import mindustry.mod.Mod;
22 |
23 | import fr.zetamap.morecommands.module.ModuleFactory;
24 | import fr.zetamap.morecommands.modules.command.CommandsModule;
25 | import fr.zetamap.morecommands.modules.effect.EffectsModule;
26 | import fr.zetamap.morecommands.modules.godmode.GodmodeModule;
27 | import fr.zetamap.morecommands.modules.help.HelpModule;
28 | import fr.zetamap.morecommands.modules.manager.ManagerModule;
29 | import fr.zetamap.morecommands.modules.messaging.MessagingModule;
30 | import fr.zetamap.morecommands.modules.misc.MiscModule;
31 | import fr.zetamap.morecommands.modules.security.AdminUsidModule;
32 | import fr.zetamap.morecommands.modules.security.AntiEvadeModule;
33 | import fr.zetamap.morecommands.modules.security.CrackedClientsModule;
34 | import fr.zetamap.morecommands.modules.security.ModerationModule;
35 | import fr.zetamap.morecommands.modules.security.PunishmentsModule;
36 | import fr.zetamap.morecommands.modules.security.ReservedNamesModule;
37 | import fr.zetamap.morecommands.modules.selector.SelectorModule;
38 | import fr.zetamap.morecommands.modules.server.SwitchModule;
39 | import fr.zetamap.morecommands.modules.tag.TagsModule;
40 | import fr.zetamap.morecommands.modules.team.TeamingModule;
41 | import fr.zetamap.morecommands.modules.tp.TeleportModule;
42 | import fr.zetamap.morecommands.modules.voting.VotingModule;
43 | import fr.zetamap.morecommands.modules.world.WorldEditModule;
44 |
45 |
46 | /** Static access to the MoreCommands modules. */
47 | public class Modules {
48 | public static ManagerModule manager;
49 | public static CommandsModule commands;
50 | public static HelpModule help;
51 | public static AdminUsidModule usid;
52 | public static MessagingModule messaging;
53 | public static VotingModule voting;
54 | public static TagsModule tags;
55 | public static EffectsModule effects;
56 | public static SwitchModule switcher;
57 | public static TeamingModule team;
58 | public static TeleportModule teleport;
59 | public static WorldEditModule worldEdit;
60 | public static MiscModule misc;
61 | public static GodmodeModule godmode;
62 | public static PunishmentsModule punishments;
63 | public static ModerationModule moderation;
64 | public static SelectorModule selector;
65 | public static CrackedClientsModule cracked;
66 | public static ReservedNamesModule reserved;
67 | public static AntiEvadeModule antiEvade;
68 |
69 | /** Creates and registers the MoreCommands modules. */
70 | public static void register(Mod context) {
71 | manager = new ManagerModule(); ModuleFactory.add(context, manager);
72 | commands = new CommandsModule(); ModuleFactory.add(context, commands);
73 | help = new HelpModule(); ModuleFactory.add(context, help);
74 | usid = new AdminUsidModule(); ModuleFactory.add(context, usid);
75 | messaging = new MessagingModule(); ModuleFactory.add(context, messaging);
76 | voting = new VotingModule(); ModuleFactory.add(context, voting);
77 | tags = new TagsModule(); ModuleFactory.add(context, tags);
78 | effects = new EffectsModule(); ModuleFactory.add(context, effects);
79 | switcher = new SwitchModule(); ModuleFactory.add(context, switcher);
80 | team = new TeamingModule(); ModuleFactory.add(context, team);
81 | teleport = new TeleportModule(); ModuleFactory.add(context, teleport);
82 | worldEdit = new WorldEditModule(); ModuleFactory.add(context, worldEdit);
83 | misc = new MiscModule(); ModuleFactory.add(context, misc);
84 | godmode = new GodmodeModule(); ModuleFactory.add(context, godmode);
85 | punishments = new PunishmentsModule(); ModuleFactory.add(context, punishments);
86 | moderation = new ModerationModule(); ModuleFactory.add(context, moderation);
87 | selector = new SelectorModule(); ModuleFactory.add(context, selector);
88 | cracked = new CrackedClientsModule(); ModuleFactory.add(context, cracked);
89 | reserved = new ReservedNamesModule(); ModuleFactory.add(context, reserved);
90 | antiEvade = new AntiEvadeModule(); ModuleFactory.add(context, antiEvade);
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/src/main/java/fr/zetamap/morecommands/modules/server/Server.java:
--------------------------------------------------------------------------------
1 | /**
2 | * This file is part of MoreCommands. The plugin that adds a bunch of commands to your server.
3 | * Copyright (c) 2021-2025 ZetaMap
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | package fr.zetamap.morecommands.modules.server;
20 |
21 | import arc.Core;
22 | import arc.func.Cons;
23 | import arc.util.Nullable;
24 |
25 | import mindustry.Vars;
26 | import mindustry.gen.Call;
27 | import mindustry.gen.Player;
28 | import mindustry.net.Host;
29 |
30 | import fr.zetamap.morecommands.util.Strings;
31 |
32 |
33 | public class Server {
34 | //protected SwitchModule module;
35 | /** Internal server name, in kebab case without any colors and glyphs. */
36 | public final String name;
37 | protected String alias, displayName, ip;
38 | protected int port;
39 | protected boolean adminOnly, pingScheduled;
40 | public Host info;
41 |
42 | public Server(String name, String address) throws IllegalArgumentException {
43 | this(name, null, null, address, false);
44 | }
45 |
46 | public Server(String name, @Nullable String alias, @Nullable String displayName, String address, boolean admin)
47 | throws IllegalArgumentException {
48 | this.name = name;
49 | this.alias = alias;
50 | this.displayName = displayName;
51 | this.adminOnly = admin;
52 | setAddress(address);
53 | }
54 |
55 | public Server(String name, String ip, int port) {
56 | this(name, null, null, ip, port, false);
57 | }
58 |
59 | public Server(String name, @Nullable String alias, @Nullable String displayName, String ip, int port, boolean admin) {
60 | this.name = name;
61 | this.alias = alias;
62 | this.displayName = displayName;
63 | this.ip = ip;
64 | this.port = port;
65 | this.adminOnly = admin;
66 | }
67 |
68 | public String alias() { return alias; }
69 | public String displayName() { return displayName; }
70 | public String ip() { return ip; }
71 | public int port() { return port; }
72 | public boolean adminOnly() { return adminOnly; }
73 |
74 | /** Parses ip:port format. */
75 | protected void setAddress(String address) {
76 | // From {@link mindustry.ui.dialogs.JoinDialog.Server#setIP(String)}.
77 | String i = address;
78 | int p = Vars.port;
79 | boolean isIpv6 = Strings.count(address, ':') > 1;
80 |
81 | if (isIpv6 && address.lastIndexOf("]:") != -1 && address.lastIndexOf("]:") != address.length()-1) {
82 | int idx = address.indexOf("]:");
83 | i = address.substring(1, idx);
84 | p = Strings.parseInt(address.substring(idx + 2));
85 | } else if (!isIpv6 && address.lastIndexOf(':') != -1 && address.lastIndexOf(':') != address.length()-1){
86 | int idx = address.lastIndexOf(':');
87 | i = address.substring(0, idx);
88 | p = Strings.parseInt(address.substring(idx+1));
89 | } else {
90 | i = address;
91 | p = Vars.port;
92 | }
93 |
94 | if (p == Integer.MIN_VALUE) {
95 | i = address;
96 | p = Vars.port;
97 | }
98 |
99 | this.ip = i;
100 | this.port = p;
101 | }
102 |
103 | /** @return {@link #ip}{@code :}{@link #port}. */
104 | public String address() {
105 | // From {@link mindustry.ui.dialogs.JoinDialog.Server#displayIP()}.
106 | return ip.indexOf(':') != -1 ? port != Vars.port ? '[' + ip + "]:" + port : ip :
107 | port != Vars.port ? ip + ':' + port : ip;
108 | }
109 |
110 | public void ping() {
111 | ping(null, null);
112 | }
113 |
114 | public synchronized void ping(Cons valid, Cons failed) {
115 | // Ignore if a ping is already scheduled
116 | if (pingScheduled) return;
117 | pingScheduled = true;
118 |
119 | Vars.net.pingHost(ip, port, h -> Core.app.post(() -> {
120 | info = h;
121 | pingScheduled = false;
122 | if (valid != null) valid.get(h);
123 | }), e -> Core.app.post(() -> {
124 | info = null;
125 | pingScheduled = false;
126 | if (failed != null) failed.get(e);
127 | }));
128 | }
129 |
130 | public void connect(Player player, Cons status) {
131 | PreConnect.Availability pre = PreConnect.verify(player.admin, this);
132 | status.get(pre);
133 | if (pre.ok()) {
134 | // Prefer information from ping when possible
135 | if (info == null) Call.connect(player.con, ip, port);
136 | else Call.connect(player.con, info.address, info.port);
137 | }
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/src/main/java/fr/zetamap/morecommands/command/ClientCommandHandler.java:
--------------------------------------------------------------------------------
1 | /**
2 | * This file is part of MoreCommands. The plugin that adds a bunch of commands to your server.
3 | * Copyright (c) 2025 ZetaMap
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | package fr.zetamap.morecommands.command;
20 |
21 | import arc.struct.ObjectSet;
22 | import arc.struct.Seq;
23 | import arc.util.CommandHandler;
24 | import arc.util.CommandHandler.*;
25 |
26 | import mindustry.gen.Player;
27 |
28 | import fr.zetamap.morecommands.PlayerData;
29 | import fr.zetamap.morecommands.misc.Players;
30 | import fr.zetamap.morecommands.util.Logger;
31 |
32 |
33 | public class ClientCommandHandler {
34 | private static final Logger logger = new Logger("Client Commands");
35 | public final CommandHandler handler;
36 | public final Seq all = new Seq<>(), admin = new Seq<>();
37 | /**
38 | * Non-exhaustive list of potential admin commands from mindustry server or some other plugins.
39 | * More Commands admin commands will be added to this list.
40 | */
41 | public final ObjectSet defaultAdminCommands = ObjectSet.with(
42 | "a", "js", "vanish", "killall", "pause", "rollback", "saves", "restart", "blacklist", "whitelist"
43 | );
44 |
45 | public ClientCommandHandler(CommandHandler handler) {
46 | this.handler = handler;
47 | }
48 |
49 | public void add(String name, String desc, CommandRunner runner) {
50 | add(name, "", desc, runner);
51 | }
52 |
53 | public void add(String name, String params, String desc, CommandRunner runner) {
54 | // If the command already exist, try to place the new at the same position
55 | int index = handler.getCommandList().indexOf(c -> c.text.equals(name));
56 | all.add(handler.register(name, params, desc, (args, player) -> {
57 | PlayerData p = getcheck(player);
58 | if (p == null) return;
59 |
60 | try { runner.accept(args, p); }
61 | catch (Exception e) {
62 | logger.err("Error while running command '@' for player '@'", e, name, player.uuid());
63 | Players.err(player, "Error while running the command. Please report this error.");
64 | }
65 | }));
66 | if (index != -1) handler.getCommandList().insert(index, handler.getCommandList().pop());
67 | }
68 |
69 | public void addAdmin(String name, String desc, CommandRunner runner) {
70 | addAdmin(name, "", desc, runner);
71 | }
72 |
73 | public void addAdmin(String name, String params, String desc, CommandRunner runner) {
74 | // If the command already exist, try to place the new at the same position
75 | int index = handler.getCommandList().indexOf(c -> c.text.equals(name));
76 | defaultAdminCommands.add(admin.add(all.add(handler.register(name, params, desc, (args, player) -> {
77 | if (!player.admin) {
78 | Players.errCommandUseDenied(player);
79 | return;
80 | }
81 |
82 | PlayerData p = getcheck(player);
83 | if (p == null) return;
84 |
85 | try { runner.accept(args, p); }
86 | catch (Exception e) {
87 | logger.err("Error while running command '@' for admin player '@'", e, name, player.uuid());
88 | Players.err(player, "Error while running the command: @", e.toString());
89 | }
90 | })).peek()).peek().text);
91 | if (index != -1) handler.getCommandList().insert(index, handler.getCommandList().pop());
92 | }
93 |
94 | private PlayerData getcheck(Player player) {
95 | PlayerData p = PlayerData.get(player);
96 | // Should never happen
97 | if (p == null) {
98 | logger.err("FATAL: Player '@' is not in PlayerData! Please report this error at: @.", player.uuid(),
99 | "https://github.com/ZetaMap/MoreCommands/issues/new");
100 | Players.err(p, "FATAL: Operation not permitted! Please report this error.");
101 | }
102 | return p;
103 | }
104 |
105 | public boolean isAdmin(String name) {
106 | return defaultAdminCommands.contains(name);
107 | }
108 |
109 | public Command get(String name) {
110 | return handler.getCommandList().find(c -> c.text.equals(name));
111 | }
112 |
113 | public void remove(String name) {
114 | handler.removeCommand(name);
115 | all.remove(c -> c.text.equals(name));
116 | admin.remove(c -> c.text.equals(name));
117 | }
118 |
119 | public void clear() {
120 | all.each(c -> handler.removeCommand(c.text));
121 | all.clear();
122 | admin.clear();
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/src/main/java/fr/zetamap/morecommands/misc/MCEvents.java:
--------------------------------------------------------------------------------
1 | /**
2 | * This file is part of MoreCommands. The plugin that adds a bunch of commands to your server.
3 | * Copyright (c) 2025 ZetaMap
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | package fr.zetamap.morecommands.misc;
20 |
21 | import arc.util.Nullable;
22 |
23 | import fr.zetamap.morecommands.PlayerData;
24 | import fr.zetamap.morecommands.modules.security.Punishment;
25 | import fr.zetamap.morecommands.modules.server.Server;
26 | import fr.zetamap.morecommands.modules.voting.PlayerVoteSession;
27 | import fr.zetamap.morecommands.modules.voting.VoteSession.VoteType;
28 |
29 |
30 | public class MCEvents {
31 | public static class PunishmentEvent {
32 | public final @Nullable PlayerData author, target;
33 | public final Punishment punishment;
34 |
35 | public PunishmentEvent(PlayerData author, PlayerData target, Punishment punishment) {
36 | this.author = author;
37 | this.target = target;
38 | this.punishment = punishment;
39 | }
40 | }
41 | public static class PunishmentPardonedEvent {
42 | public final @Nullable PlayerData author;
43 | public final Punishment punishment;
44 | public final Punishment.Pardon pardon;
45 |
46 | public PunishmentPardonedEvent(PlayerData author, Punishment punishment, Punishment.Pardon pardon) {
47 | this.author = author;
48 | this.punishment = punishment;
49 | this.pardon = pardon;
50 | }
51 | }
52 | /*
53 | /** Only fired for online players. *\/
54 | public static class PunishmentExpiredEvent {
55 | public final PlayerData player;
56 | public final Punishment punishment;
57 |
58 | public PunishmentExpiredEvent(PlayerData player, Punishment punishment) {
59 | this.player = player;
60 | this.punishment = punishment;
61 | }
62 | }
63 | */
64 |
65 | public static class GatekeeperProcessStartedEvent {
66 | public final Gatekeeper.Context context;
67 |
68 | public GatekeeperProcessStartedEvent(Gatekeeper.Context context) {
69 | this.context = context;
70 | }
71 | }
72 | /** Called after a processor finished. Instance is reused while processing a client, do not nest! */
73 | public static class GatekeeperProcessedEvent {
74 | public String name;
75 | public Gatekeeper.Priority priority;
76 | public Gatekeeper.Result result;
77 | public final Gatekeeper.Context context;
78 |
79 | public GatekeeperProcessedEvent(Gatekeeper.Context context) {
80 | this.context = context;
81 | }
82 |
83 | public GatekeeperProcessedEvent set(String name, Gatekeeper.Priority priority, Gatekeeper.Result result) {
84 | this.name = name;
85 | this.priority = priority;
86 | this.result = result;
87 | return this;
88 | }
89 | }
90 |
91 | public static class PlayerSwitchedEvent {
92 | public final PlayerData player;
93 | public final Server server;
94 |
95 | public PlayerSwitchedEvent(PlayerData player, Server server) {
96 | this.player = player;
97 | this.server = server;
98 | }
99 | }
100 |
101 | public static class VoteSessionStartedEvent {
102 | public final PlayerVoteSession> session;
103 | public final PlayerData author;
104 |
105 | public VoteSessionStartedEvent(PlayerVoteSession> session, PlayerData author) {
106 | this.session = session;
107 | this.author = author;
108 | }
109 | }
110 | public static class VoteSessionVotedEvent {
111 | public final PlayerVoteSession> session;
112 | public final PlayerVoteSession.VoteType type;
113 | public final PlayerData player;
114 |
115 | public VoteSessionVotedEvent(PlayerVoteSession> session, VoteType type, PlayerData player) {
116 | this.session = session;
117 | this.type = type;
118 | this.player = player;
119 | }
120 | }
121 | public static class VoteSessionClosedEvent {
122 | public final PlayerVoteSession> session;
123 | public final @Nullable PlayerData author;
124 | public final boolean passed;
125 |
126 | public VoteSessionClosedEvent(PlayerVoteSession> session, PlayerData author, boolean passed) {
127 | this.session = session;
128 | this.author = author;
129 | this.passed = passed;
130 | }
131 |
132 | public boolean passed() { return passed && author == null; }
133 | public boolean forced() { return passed && author != null; }
134 | public boolean failed() { return !passed && author == null; }
135 | public boolean canceled() { return !passed && author != null; }
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/src/main/java/fr/zetamap/morecommands/util/VersionChecker.java:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Anti-VPN-Service (AVS). The plugin securing your server against VPNs.
3 | *
4 | * MIT License
5 | *
6 | * Copyright (c) 2024-2025 Xpdustry
7 | *
8 | * Permission is hereby granted, free of charge, to any person obtaining a copy
9 | * of this software and associated documentation files (the "Software"), to deal
10 | * in the Software without restriction, including without limitation the rights
11 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 | * copies of the Software, and to permit persons to whom the Software is
13 | * furnished to do so, subject to the following conditions:
14 | *
15 | * The above copyright notice and this permission notice shall be included in all
16 | * copies or substantial portions of the Software.
17 | *
18 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24 | * SOFTWARE.
25 | */
26 |
27 | package fr.zetamap.morecommands.util;
28 |
29 | import arc.util.Http;
30 |
31 | import mindustry.Vars;
32 | import mindustry.mod.Mod;
33 | import mindustry.mod.Mods;
34 |
35 |
36 | public class VersionChecker {
37 | private static final Logger logger = new Logger("Updater");
38 |
39 | public static String keyToFind = "tag_name";
40 | public static String repoLinkFormat = "https://github.com/@/releases/latest";
41 | public static String repoApiLinkFormat = mindustry.Vars.ghApi + "/repos/@/releases/latest";
42 |
43 | public static UpdateState checkFor(T mod) { return checkFor(mod, true); }
44 |
45 | public static UpdateState checkFor(T mod, boolean promptStatus) {
46 | Mods.LoadedMod load = Vars.mods.getMod(mod.getClass());
47 | if(load == null) throw new IllegalArgumentException("Mod is not loaded yet (or missing)!");
48 | return checkFor(load.meta);
49 | }
50 |
51 | public static UpdateState checkFor(Mods.ModMeta mod) { return checkFor(mod, true); }
52 | /**
53 | * Check for update using the "version" and "repo" properties
54 | * in the mod/plugin definition (<plugin/mod>.[h]json).
55 | *
56 | * The github repo must be formatted like that "{@code /}".
57 | * The version must be formatted like that "{@code 146.2}" and can starts with "{@code v}",
58 | * but must not contains letters, like "{@code beta}" or "{@code -dev}".
59 | *
60 | * @return the update state of the mod
61 | */
62 | public static UpdateState checkFor(Mods.ModMeta mod, boolean promptStatus) {
63 | if (promptStatus) logger.info("Checking for updates...");
64 |
65 | if (mod.repo == null || mod.repo.isEmpty() || mod.repo.indexOf('/') == -1) {
66 | if (promptStatus) logger.warn("No repo found for an update.");
67 | return UpdateState.missing;
68 | } else if (mod.version == null || mod.version.isEmpty()) {
69 | if (promptStatus) logger.warn("No current version found for an update.");
70 | return UpdateState.missing;
71 | }
72 |
73 | UpdateState[] status = {UpdateState.error};
74 | Http.get(Strings.format(repoApiLinkFormat, mod.repo))
75 | .timeout(5000)
76 | .error(failure -> {
77 | if (promptStatus) logger.err("Unable to check for updates: @", failure.getLocalizedMessage());
78 | }).block(success -> {
79 | String content = success.getResultAsString();
80 | if (content.isBlank()) {
81 | if (promptStatus) logger.err("Unable to check for updates: no content received.");
82 | return;
83 | }
84 |
85 | // Extract the version
86 | String tagName;
87 | try { tagName = new arc.util.serialization.JsonReader().parse(content).getString(keyToFind); }
88 | catch (Exception e) {
89 | if (promptStatus) {
90 | logger.err("Unable to check for updates: invalid Json or missing key 'tag_name'.");
91 | logger.err("Error: @", e.getLocalizedMessage());
92 | }
93 | return;
94 | }
95 |
96 | // Compare the version
97 | if (promptStatus) logger.info("Found version: @. Current version: @", tagName, mod.version);
98 | if (Strings.isVersionAtLeast(mod.version, tagName)) {
99 | if (promptStatus) logger.info("Check out this link to upgrade @: @", mod.displayName,
100 | Strings.format(repoLinkFormat, mod.repo));
101 | status[0] = UpdateState.outdated;
102 | } else {
103 | if (promptStatus) logger.info("Already up-to-date, no need to update.");
104 | status[0] = UpdateState.uptodate;
105 | }
106 | });
107 |
108 | return status[0];
109 | }
110 |
111 |
112 | public static enum UpdateState {
113 | /** "version" or/and "repo" properties are missing in the mod/plugin definition. */
114 | missing,
115 | /** Error while checking for updates. */
116 | error,
117 | /** No new updates found, it's the latest version. */
118 | uptodate,
119 | /** An update was found, the mod/plugin needs to be upgraded. */
120 | outdated
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/src/main/java/fr/zetamap/morecommands/modules/selector/Selector.java:
--------------------------------------------------------------------------------
1 | /**
2 | * This file is part of MoreCommands. The plugin that adds a bunch of commands to your server.
3 | * Copyright (c) 2025 ZetaMap
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | package fr.zetamap.morecommands.modules.selector;
20 |
21 | import arc.math.geom.Vec2;
22 | import arc.struct.Seq;
23 |
24 | import mindustry.gen.Groups;
25 | import mindustry.gen.Player;
26 | import mindustry.gen.Unit;
27 |
28 |
29 | public class Selector {
30 | /** Common extractors. */
31 | public static final Extractor
32 | /** Only players. */
33 | playerExtractor = (l, e) -> Groups.player.each(p -> l.add(p.unit())),
34 | /** Only units. */
35 | unitExtractor = (l, e) -> Groups.unit.each(u -> !u.isPlayer(), l::add),
36 | /** Unit and players. */
37 | entityExtractor = (l, e) -> Groups.unit.copy(l);
38 |
39 | public final String name;
40 | /** Less than {@code 0} means no limit. */
41 | public final int limit;
42 | public final Sorting sorting;
43 | public final Extractor extractor;
44 | public final int id;
45 |
46 | public Selector(String name, Extractor extractor) { this(name, 0, Sorting.arbitrary, extractor); }
47 | public Selector(String name, int limit, Extractor extractor) { this(name, limit, Sorting.arbitrary, extractor); }
48 | public Selector(String name, Sorting sorting, Extractor extractor) { this(name, 0, sorting, extractor); }
49 | /**
50 | * @param name the selector's name/alias.
51 | * @param one whether this selector will select only one entity.
52 | * @param sorting sorting to do after selected the units. {@code null} for no sorting.
53 | * @param extractor the entity extractor
54 | */
55 | public Selector(String name, int limit, Sorting sorting, Extractor extractor) {
56 | this.name = name;
57 | this.limit = limit;
58 | this.sorting = sorting;
59 | this.extractor = extractor;
60 | this.id = Selectors.add(this);
61 | }
62 |
63 | public Seq select(Player executor) { return select(executor, null); }
64 | /**
65 | * If {@code executor} is {@code null} or {@link Player#dead()}, a zero position is used.
66 | * And, in this case, if {@code bounding} properties are presents, {@code positioning} properties must also be presents,
67 | * else an {@link IllegalArgumentException} is thrown.
68 | */
69 | public Seq select(Player executor, Seq properties) {
70 | Seq selected = new Seq<>(false); // First unordered for optimization purposes
71 | Vec2 pos = new Vec2();
72 | boolean hasProps = properties != null && properties.any(),
73 | needSorting = sorting != null,
74 | needLimiting = limit > 0;
75 |
76 | if (hasProps) properties.sort(p -> p.property.category.ordinal());
77 |
78 | if (executor != null && !executor.dead()) pos.set(executor);
79 | else if (hasProps &&
80 | properties.contains(p -> p.property.category == SelectorProperty.Category.bounding) &&
81 | !properties.contains(p -> p.property.category == SelectorProperty.Category.positioning))
82 | throw new IllegalArgumentException("Unable to find executor position. Please add positioning properties.");
83 |
84 | extractor.select(selected, executor);
85 | // Removes null units. Can happen when a selector target dead players
86 | selected.removeAll(u -> u == null);
87 |
88 | if (hasProps) {
89 | int resume = -1;
90 | // Apply properties updates and stop at sorting and limiting properties
91 | for (int i=0; i SelectorProperty.Category.other.ordinal()) {
94 | resume = i;
95 | break;
96 | }
97 | p.update(this, selected, executor, pos);
98 | }
99 | // Apply filtering
100 | selected.removeAll(e -> !properties.allMatch(p -> p.passes(this, executor, pos, e)));
101 | // Resume iteration for sorting and limiting properties
102 | for (int i=resume; i>=0 && i out, Player executor);
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/src/main/java/fr/zetamap/morecommands/util/Autosaver.java:
--------------------------------------------------------------------------------
1 | /**
2 | * This file is part of MoreCommands. The plugin that adds a bunch of commands to your server.
3 | * Copyright (c) 2025 ZetaMap
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | package fr.zetamap.morecommands.util;
20 |
21 | import arc.ApplicationListener;
22 | import arc.Core;
23 | import arc.func.Cons2;
24 | import arc.struct.Seq;
25 | import arc.util.Interval;
26 |
27 |
28 | /** Save periodically the registered {@link Saveable}s and also when the application exit. */
29 | public class Autosaver {
30 | protected final static Logger logger = new Logger(Autosaver.class);
31 | protected static ApplicationListener listener;
32 | protected static int spacing = 360 * 60; // in ticks
33 | /**
34 | * Doesn't log a message when starting/stopping the {@link Autosaver} or when running an auto save.
35 | * Errors will always be logged.
36 | */
37 | public static boolean silent;
38 | /** Called when a save fail. */
39 | public static Cons2 errorHandler;
40 |
41 |
42 | /** Adds to the {@code normal} priority. */
43 | public static void add(Saveable saveable) {
44 | add(saveable, SavePriority.normal);
45 | }
46 |
47 | public static void add(Saveable saveable, SavePriority priority) {
48 | remove(saveable);
49 | priority.saves.add(saveable);
50 | }
51 |
52 | public static void remove(Saveable saveable) {
53 | for (SavePriority p : SavePriority.all) {
54 | if (p.saves.remove(saveable)) break;
55 | }
56 | }
57 |
58 | public static void clear() {
59 | for (SavePriority p : SavePriority.all) clear(p);
60 | }
61 |
62 | public static void clear(SavePriority priority) {
63 | priority.saves.clear();
64 | }
65 |
66 | public static boolean has(Saveable saveable) {
67 | return priorityOf(saveable) != null;
68 | }
69 |
70 | public static boolean has(Saveable saveable, SavePriority priority) {
71 | return priority.saves.contains(saveable);
72 | }
73 |
74 | public static SavePriority priorityOf(Saveable saveable) {
75 | for (SavePriority p : SavePriority.all) {
76 | if (has(saveable, p)) return p;
77 | }
78 | return null;
79 | }
80 |
81 | public static boolean saveNeeded() {
82 | for (SavePriority p : SavePriority.all) {
83 | if (p.saves.contains(Saveable::modified)) return true;
84 | }
85 | return false;
86 | }
87 |
88 | /** Save all registered things now (only if modified). Errors are ignored and just printed. */
89 | public static boolean save() {
90 | if (!saveNeeded()) return false;
91 | if (!silent) logger.info("Running autosave...");
92 | for (SavePriority p : SavePriority.all) {
93 | p.saves.each(Saveable::modified, s -> {
94 | if (!silent) logger.debug("Saving @.", s.name());
95 | try { s.save(); }
96 | catch (Throwable t) {
97 | logger.err("Failed to save @", t, s.name());
98 | if (errorHandler != null) errorHandler.get(s, t);
99 | }
100 | });
101 | }
102 | if (!silent) logger.info("Autosave completed.");
103 | return true;
104 | }
105 |
106 | /** Start the auto saver, will also save on application exit. */
107 | public static boolean start() {
108 | if (isStarted()) return false;
109 | Core.app.addListener(listener = new ApplicationListener() {
110 | Interval timer = new Interval();
111 |
112 | @Override
113 | public void update() {
114 | if (timer.get(spacing)) save();
115 | }
116 |
117 | @Override
118 | public void dispose() {
119 | save();
120 | stop();
121 | }
122 | });
123 | if (!silent) logger.info("Autosaver started!");
124 | return true;
125 | }
126 |
127 | public static boolean stop() {
128 | if (!isStarted()) return false;
129 | Core.app.removeListener(listener);
130 | listener = null;
131 | if (!silent) logger.info("Autosaver stopped!");
132 | return true;
133 | }
134 |
135 | public static boolean isStarted() {
136 | return listener != null;
137 | }
138 |
139 | public static int spacing() {
140 | return spacing / 60;
141 | }
142 |
143 | public static void spacing(int spacing) {
144 | if (spacing < 1) throw new IllegalArgumentException("spacing must be greater than 1 second");
145 | Autosaver.spacing = spacing * 60;
146 | }
147 |
148 |
149 | /** Defines a things that can be saved by the {@link Autosaver}. */
150 | public static interface Saveable {
151 | /** Used for logging. */
152 | String name();
153 | boolean modified();
154 | void save();
155 | }
156 |
157 |
158 | /** Defines the order to save things. */
159 | public static enum SavePriority {
160 | high, normal, low;
161 |
162 | static final SavePriority[] all = values();
163 | // More simple to store the saveable things here.
164 | // Because the priority should not be modified after registration.
165 | final Seq saves = new Seq<>();
166 | }
167 | }
168 |
--------------------------------------------------------------------------------
/src/main/java/fr/zetamap/morecommands/misc/Range.java:
--------------------------------------------------------------------------------
1 | /**
2 | * This file is part of MoreCommands. The plugin that adds a bunch of commands to your server.
3 | * Copyright (c) 2025 ZetaMap
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 |
20 | package fr.zetamap.morecommands.misc;
21 |
22 |
23 | public interface Range> {
24 | T min();
25 | T max();
26 |
27 | default boolean contains(T value) {
28 | return value != null
29 | && (min() == null || value.compareTo(min()) >= 0)
30 | && (max() == null || value.compareTo(max()) <= 0);
31 | }
32 |
33 | /** @return {@link #min()}{@code ..}{@link #max()} or {@link #min()} if they are equals. */
34 | default String asString() {
35 | T min = min(), max = max();
36 | return min.equals(max) ? String.valueOf(min) : min + ".." + max;
37 | }
38 |
39 |
40 | public static interface Fixed> extends Range {
41 | T value();
42 |
43 | default T min() { return value(); }
44 | default T max() { return value(); }
45 | default boolean contains(T v) { return v != null && (value() == null || v.compareTo(value()) == 0); }
46 | default String asString() { return String.valueOf(value()); }
47 | }
48 |
49 |
50 | /** A {@code null} {@code min} or {@code max} means no limit for the side (or both). */
51 | public static Range of(Integer min, Integer max) {
52 | if (min != null && max != null && min > max) throw new IllegalArgumentException("min is greater than max");
53 | return new Range() {
54 | public Integer min() { return min; }
55 | public Integer max() { return max; }
56 | public boolean contains(Integer value) {
57 | return value != null && (min == null || value >= min) && (max == null || value <= max);
58 | }
59 | public String toString() { return asString(); }
60 | };
61 | }
62 |
63 | public static Range.Fixed fixed(int value) {
64 | return new Range.Fixed() {
65 | public Integer value() { return value; }
66 | public boolean contains(Integer v) { return v != null && v == value; }
67 | public String toString() { return asString(); }
68 | };
69 | }
70 |
71 | /** A {@code null} {@code min} or {@code max} means no limit for the side (or both). */
72 | public static Range of(Long min, Long max) {
73 | if (min != null && max != null && min > max) throw new IllegalArgumentException("min is greater than max");
74 | return new Range() {
75 | public Long min() { return min; }
76 | public Long max() { return max; }
77 | public boolean contains(Long value) {
78 | return value != null && (min == null || value >= min) && (max == null || value <= max);
79 | }
80 | public String toString() { return asString(); }
81 | };
82 | }
83 |
84 | public static Range.Fixed fixed(long value) {
85 | return new Range.Fixed() {
86 | public Long value() { return value; }
87 | public boolean contains(Long v) { return v != null && v == value; }
88 | public String toString() { return asString(); }
89 | };
90 | }
91 |
92 | /** A {@code null} {@code min} or {@code max} means no limit for the side (or both). */
93 | public static Range of(Float min, Float max) {
94 | if (min != null && max != null && min > max) throw new IllegalArgumentException("min is greater than max");
95 | return new Range() {
96 | public Float min() { return min; }
97 | public Float max() { return max; }
98 | public boolean contains(Float value) {
99 | return value != null && (min == null || value >= min) && (max == null || value <= max);
100 | }
101 | public String toString() { return asString(); }
102 | };
103 | }
104 |
105 | public static Range.Fixed fixed(float value) {
106 | return new Range.Fixed() {
107 | public Float value() { return value; }
108 | public boolean contains(Float v) { return v != null && v == value; }
109 | public String toString() { return asString(); }
110 | };
111 | }
112 |
113 | /** A {@code null} {@code min} or {@code max} means no limit for the side (or both). */
114 | public static Range of(Double min, Double max) {
115 | if (min != null && max != null && min > max) throw new IllegalArgumentException("min is greater than max");
116 | return new Range() {
117 | public Double min() { return min; }
118 | public Double max() { return max; }
119 | public boolean contains(Double value) {
120 | return value != null && (min == null || value >= min) && (max == null || value <= max);
121 | }
122 | public String toString() { return asString(); }
123 | };
124 | }
125 |
126 | public static Range.Fixed fixed(double value) {
127 | return new Range.Fixed() {
128 | public Double value() { return value; }
129 | public boolean contains(Double v) { return v != null && v == value; }
130 | public String toString() { return asString(); }
131 | };
132 | }
133 | }
--------------------------------------------------------------------------------
/src/main/java/fr/zetamap/morecommands/misc/CoordinatesParser.java:
--------------------------------------------------------------------------------
1 | /**
2 | * This file is part of MoreCommands. The plugin that adds a bunch of commands to your server.
3 | * Copyright (c) 2021-2025 ZetaMap
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | package fr.zetamap.morecommands.misc;
20 |
21 | import java.util.regex.Pattern;
22 |
23 | import arc.math.geom.Vec2;
24 |
25 | import fr.zetamap.morecommands.PlayerData;
26 | import fr.zetamap.morecommands.util.Strings;
27 |
28 |
29 | /**
30 | * Minecraft-like coordinates parser, but coordinates are separated with a comma instead of a space,
31 | * and a single {@code '~'} can be used to specify both x and y axis.
32 | */
33 | public class CoordinatesParser {
34 | private static final Pattern quotes = Pattern.compile("'(.*?)'");
35 | public static final char worldRelativePrefix = '~', separator = ',';
36 |
37 | public final PlayerData target;
38 | public final Vec2 pos = new Vec2();
39 | public final boolean byCoordinates;
40 | public final String[] rest;
41 |
42 | public CoordinatesParser(PlayerData executor, String[] args) throws IllegalArgumentException {
43 | this(executor, args, 0, args.length);
44 | }
45 |
46 | public CoordinatesParser(PlayerData executor, String[] args, int from, int to) throws IllegalArgumentException {
47 | if (args.length == 0 || from < 0 || to > args.length || from >= to || args[from].isEmpty())
48 | throw new IllegalArgumentException("Missing coordinates or player name/uuid");
49 |
50 | String coor = args[from];
51 | int length = coor.length();
52 |
53 | // Avoid to search a player if it's looks like coordinates
54 | if (!isRelativeCoordinate(coor, 0, length)) {
55 | Players.SearchResult result = Players.find(args, from, to);
56 | if (result.found) {
57 | if (result.player.player.dead())
58 | throw new IllegalArgumentException("Unable to find target player position");
59 | target = result.player;
60 | pos.set(result.player.player);
61 | byCoordinates = false;
62 | rest = result.rest;
63 | return;
64 | }
65 | }
66 |
67 | int comma = coor.indexOf(separator);
68 | float x, y;
69 |
70 | if (comma != -1) {
71 | x = parseCoordinate(executor, coor, 0, comma == -1 ? length : comma, executor.player.x);
72 | if (comma == length-1) throw new IllegalArgumentException("Missing 'y' axis after comma");
73 | y = parseCoordinate(executor, coor, comma+1, length, executor.player.y);
74 | } else if (isRelativeCoordinate(coor, 0, length)) {
75 | if (executor.player.dead()) throw new IllegalArgumentException("Unable to find player position");
76 | x = y = parseWorldCoordinate(coor, 1, length);
77 | x += executor.player.x;
78 | y += executor.player.y;
79 | } else {
80 | if (Strings.parseInt(coor, 10, Integer.MIN_VALUE, 0, comma) != Integer.MIN_VALUE)
81 | throw new IllegalArgumentException("Missing 'y' axis");
82 | else throw new IllegalArgumentException("Invalid coordinates or player not found");
83 | }
84 |
85 |
86 | target = executor;
87 | pos.set(x, y);
88 | byCoordinates = true;
89 | rest = java.util.Arrays.copyOfRange(args, from+1, to);
90 | }
91 |
92 | private static boolean isRelativeCoordinate(String arg, int from, int to) {
93 | return arg.charAt(from) == worldRelativePrefix;
94 | }
95 |
96 | private static float parseCoordinate(PlayerData executor, String arg, int from, int to, float base) {
97 | if (to <= from) throw new IllegalArgumentException("Invalid coordinates or player not found");
98 | if (isRelativeCoordinate(arg, from, to)) {
99 | // Check whether the player is dead, because relative coordinates will be wrong
100 | if (executor.player.dead()) throw new IllegalArgumentException("Unable to find player position");
101 | return base + parseWorldCoordinate(arg, from+1, to);
102 | }
103 | return parseWorldCoordinate(arg, from, to);
104 | }
105 |
106 | private static float parseWorldCoordinate(String arg, int from, int to) {
107 | if (to <= from) return 0f;
108 | int offset = Strings.parseInt(arg, 10, Integer.MIN_VALUE, from, to);
109 | if (offset == Integer.MIN_VALUE) throw new IllegalArgumentException("Invalid coordinates or player not found");
110 | return offset * mindustry.Vars.tilesize; // scale
111 | }
112 |
113 | public static CoordinatesParser parse(PlayerData executor, String[] args) { return parse(executor, args, 0, args.length); }
114 | public static CoordinatesParser parse(PlayerData executor, String[] args, int from, int to) {
115 | try { return new CoordinatesParser(executor, args, from, to); }
116 | catch (Exception e) {
117 | String message = e.getMessage();
118 | if (message == null || message.isEmpty()) message = e.getClass().getSimpleName();
119 | else {
120 | if (message.charAt(message.length()-1) != '.') message += '.';
121 | message = quotes.matcher(message).replaceAll("'[orange]$1[]'");
122 | }
123 | Players.err(executor, message);
124 | }
125 | return null;
126 | }
127 | }
--------------------------------------------------------------------------------
/src/main/java/fr/zetamap/morecommands/modules/security/ReservedNamesModule.java:
--------------------------------------------------------------------------------
1 | /**
2 | * This file is part of MoreCommands. The plugin that adds a bunch of commands to your server.
3 | * Copyright (c) 2025 ZetaMap
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | package fr.zetamap.morecommands.modules.security;
20 |
21 | import arc.struct.Seq;
22 |
23 | import mindustry.Vars;
24 | import mindustry.net.Packets.KickReason;
25 |
26 | import fr.zetamap.morecommands.command.ServerCommandHandler;
27 | import fr.zetamap.morecommands.misc.Gatekeeper;
28 | import fr.zetamap.morecommands.module.AbstractSaveableModule;
29 | import fr.zetamap.morecommands.util.JsonSettings;
30 | import fr.zetamap.morecommands.util.Strings;
31 |
32 |
33 | public class ReservedNamesModule extends AbstractSaveableModule {
34 | private boolean enabled;
35 | private String message;
36 |
37 | public final Seq names = new Seq<>();
38 |
39 | public boolean add(String name) {
40 | setModified();
41 | return names.addUnique(clean(name));
42 | }
43 |
44 | public boolean remove(String name) {
45 | setModified();
46 | return names.remove(clean(name));
47 | }
48 |
49 | public boolean isReserved(String name) {
50 | name = clean(name);
51 | return names.contains(name::contains);
52 | }
53 |
54 | /** Removes colors and glyphs, and lower case the name. */
55 | public String clean(String name) {
56 | return Strings.normalize(name).toLowerCase();
57 | }
58 |
59 | public boolean enabled() {
60 | return enabled;
61 | }
62 |
63 | public void enable() {
64 | enabled = true;
65 | setModified();
66 | }
67 |
68 | public void disable() {
69 | enabled = false;
70 | setModified();
71 | }
72 |
73 | public String kickMessage() {
74 | return message;
75 | }
76 |
77 | public void kickMessage(String message) {
78 | this.message = message;
79 | setModified();
80 | }
81 |
82 | @Override
83 | protected void initImpl() {
84 | Gatekeeper.add(internalName(), Gatekeeper.Priority.low, ctx ->
85 | enabled() && !Vars.netServer.admins.isAdmin(ctx.uuid, ctx.usid) && isReserved(ctx.strippedName) ?
86 | kickMessage() == null ? Gatekeeper.reject(KickReason.nameInUse) : Gatekeeper.reject(kickMessage()) :
87 | Gatekeeper.accept());
88 | }
89 |
90 | @SuppressWarnings("unchecked")
91 | @Override
92 | protected void loadImpl(JsonSettings settings) {
93 | enabled = settings.getBool("enabled", true);
94 | message = settings.getString("message", "This nickname (or a part) is reserved, please choose another one.");
95 | names.clear();
96 | names.addAll(settings.getOrPut("names", Seq.class, String.class, Seq::new));
97 | }
98 |
99 | @Override
100 | protected void saveImpl(JsonSettings settings) {
101 | settings.put("enabled", enabled);
102 | settings.put("message", message);
103 | settings.put("names", String.class, names);
104 | }
105 |
106 | public void registerServerCommands(ServerCommandHandler handler) {
107 | handler.add("reserved-names", "[on|off|add|remove|message] [name|text...]", "Reserved nicknames can only be used by admins.",
108 | args -> {
109 | if (args.length == 0) {
110 | logger.info("&fiNote: The case, colors and glyphs are ignored and removed during the nickname check.");
111 | logger.info("Kick message: @", message == null ? "&fi(empty)" : message);
112 | if (names.isEmpty())
113 | logger.info("Reserved Nicknames: [@, @]", enabled ? "&fb&lgenabled&fr" : "&fb&lrdisabled&fr", "empty");
114 | else {
115 | logger.info("Reserved Nicknames: [@, total: @]", enabled ? "&fb&lgenabled&fr" : "&fb&lrdisabled&fr", names.size);
116 | names.each(n -> logger.info("&lk|&fr @", n));
117 | }
118 |
119 | } else if (args[0].equals("add")) {
120 | if (args.length == 1) logger.err("Missing 'name' argument.");
121 | else if (add(args[1])) logger.info("Nickname added to the list.");
122 | else logger.info("Nickname already in the list.");
123 |
124 | } else if (args[0].equals("remove")) {
125 | if (args.length == 1) logger.err("Missing 'name' argument.");
126 | else if (remove(args[1])) logger.info("Nickname removed from the list.");
127 | else logger.info("Nickname not in the list.");
128 |
129 | } else if (args[0].equals("message")) {
130 | if (args.length == 1) logger.err("Missing 'text' argument.");
131 | else if (args[1].equals("\"\"")) {
132 | kickMessage(null);
133 | logger.info("Kick message removed.");
134 | } else {
135 | kickMessage(args[1]);
136 | logger.info("Kick message modified.");
137 | }
138 |
139 | } else if (Strings.isTrue(args[0])) {
140 | enable();
141 | logger.info("Reserved nicknames list enabled.");
142 |
143 | } else if (Strings.isFalse(args[0])) {
144 | disable();
145 | logger.info("Reserved nicknames list disabled.");
146 |
147 | } else logger.err("Invalid argument! Must be 'on', 'off', 'add', 'remove' or 'message'.");
148 | });
149 | }
150 | }
151 |
--------------------------------------------------------------------------------
/src/main/java/fr/zetamap/morecommands/modules/security/AntiEvadeModule.java:
--------------------------------------------------------------------------------
1 | /**
2 | * This file is part of MoreCommands. The plugin that adds a bunch of commands to your server.
3 | * Copyright (c) 2025 ZetaMap
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | package fr.zetamap.morecommands.modules.security;
20 |
21 | import arc.Events;
22 | import arc.struct.ObjectMap;
23 | import arc.util.Time;
24 | import arc.util.Timer;
25 | import arc.util.pooling.Pool;
26 | import arc.util.pooling.Pools;
27 |
28 | import mindustry.game.EventType;
29 |
30 | import fr.zetamap.morecommands.PlayerData;
31 | import fr.zetamap.morecommands.command.ServerCommandHandler;
32 | import fr.zetamap.morecommands.misc.Players;
33 | import fr.zetamap.morecommands.module.AbstractSaveableModule;
34 | import fr.zetamap.morecommands.util.DurationFormatter;
35 | import fr.zetamap.morecommands.util.JsonSettings;
36 | import fr.zetamap.morecommands.util.Strings;
37 |
38 |
39 | public class AntiEvadeModule extends AbstractSaveableModule {
40 | private Timer.Task cleaner;
41 | private float retainTime = 1 * 60; // in minutes
42 |
43 | public final ObjectMap quits = new ObjectMap<>();
44 |
45 | /** Clear the cache. */
46 | public void clearCache() {
47 | quits.clear();
48 | }
49 |
50 | /** Removes entries older than {@link #retainTime}. */
51 | public void cleanupCache() {
52 | ObjectMap.Values v = quits.values();
53 | long now = Time.millis(), retain = retainMillis();
54 | while (v.hasNext()) {
55 | CacheEntry e = v.next();
56 | if (now - e.time >= retain) v.remove();
57 | }
58 | }
59 |
60 | /** @return cache retain in minutes. */
61 | public float retain() {
62 | return retainTime;
63 | }
64 |
65 | public long retainMillis() {
66 | return (long)(retainTime * 60 * 1000);
67 | }
68 |
69 | public void setRetain(float minutes) {
70 | retainTime = Math.max(minutes, 1);
71 | setModified();
72 | }
73 |
74 | @Override
75 | protected void initImpl() {
76 | Events.on(EventType.PlayerJoin.class, e -> {
77 | if (isDisposed()) return;
78 | CacheEntry entry = quits.get(e.player.uuid());
79 | if (entry == null) return;
80 | String name = entry.name;
81 | Pools.free(quits.remove(e.player.uuid()));
82 | if (e.player.admin) return;
83 | PlayerData player = PlayerData.get(e.player);
84 | if (player.stripedName.equals(Strings.normalize(name))) return;
85 | PlayerData.each(p -> p != player, p ->
86 | Players.warn(p, "[scarlet]Warning[]: the player @[orange] has changed his name. He was @[orange].",
87 | player.getName(), name));
88 | });
89 |
90 | Events.on(EventType.PlayerLeave.class, e -> {
91 | if (isDisposed() || e.player.admin) return; // ignore admins
92 | CacheEntry entry = Pools.obtain(CacheEntry.class, CacheEntry::new)
93 | .set(e.player.uuid(), PlayerData.get(e.player).getName(), Time.millis());
94 | quits.put(e.player.uuid(), entry);
95 | });
96 |
97 | // Clean the cache every minutes
98 | cleaner = Timer.schedule(this::cleanupCache, 60, 60);
99 | }
100 |
101 | @Override
102 | protected void disposeImpl() {
103 | cleaner.cancel();
104 | quits.clear();
105 | }
106 |
107 | @Override
108 | protected void loadImpl(JsonSettings settings) {
109 | retainTime = settings.getFloat("retain", 1 * 60);
110 | }
111 |
112 | @Override
113 | protected void saveImpl(JsonSettings settings) {
114 | settings.put("retain", retainTime);
115 | }
116 |
117 | @Override
118 | public void registerServerCommands(ServerCommandHandler handler) {
119 | handler.add("anti-evade", "[clear|minutes]", "Control the anti evade system.", args -> {
120 | if (args.length == 0) {
121 | logger.info("Anti evade is a system that notify players when one quit and reconnect with another nickname.");
122 | logger.info("This system will ignore admin players and will retain nicknames @ after disconnection.",
123 | DurationFormatter.format(retainMillis()));
124 | } else if (args[0].equals("clear")) {
125 | clearCache();
126 | logger.info("Cache cleared.");
127 | } else {
128 | int minutes = Strings.parseInt(args[0]);
129 | if (minutes == Integer.MIN_VALUE) {
130 | logger.err("Invalid argument! Must be 'clear' or a number of minutes.");
131 | return;
132 | } else if (minutes < 1) {
133 | logger.err("Should be greater than 1 minute.");
134 | return;
135 | }
136 | setRetain(minutes);
137 | logger.info("Nicknames will be retained for @ after disconnection.", DurationFormatter.format(retainMillis()));
138 | }
139 | });
140 | }
141 |
142 |
143 | public static class CacheEntry implements Pool.Poolable {
144 | public String uuid, name;
145 | public long time;
146 |
147 | CacheEntry set(String uuid, String name, long time) {
148 | this.uuid = uuid;
149 | this.name = name;
150 | this.time = time;
151 | return this;
152 | }
153 |
154 | public void reset() {
155 | uuid = name = null;
156 | time = 0;
157 | }
158 | }
159 | }
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/src/main/java/fr/zetamap/morecommands/modules/godmode/GodmodeModule.java:
--------------------------------------------------------------------------------
1 | /**
2 | * This file is part of MoreCommands. The plugin that adds a bunch of commands to your server.
3 | * Copyright (c) 2025 ZetaMap
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | package fr.zetamap.morecommands.modules.godmode;
20 |
21 | import arc.Events;
22 |
23 | import mindustry.Vars;
24 | import mindustry.game.EventType;
25 | import mindustry.gen.Call;
26 | import mindustry.gen.Unitc;
27 | import mindustry.net.Administration.ActionType;
28 | import mindustry.world.blocks.ConstructBlock;
29 |
30 | import fr.zetamap.morecommands.Modules;
31 | import fr.zetamap.morecommands.PlayerData;
32 | import fr.zetamap.morecommands.command.ClientCommandHandler;
33 | import fr.zetamap.morecommands.misc.Players;
34 | import fr.zetamap.morecommands.module.AbstractModule;
35 | import fr.zetamap.morecommands.modules.selector.SelectorParser;
36 | import fr.zetamap.morecommands.util.Strings;
37 |
38 |
39 | public class GodmodeModule extends AbstractModule {
40 | public void setGodmode(PlayerData player, boolean enabled) {
41 | player.inGodmode = enabled;
42 | if (player.player.dead()) return;
43 | player.player.unit().health = enabled ? Float.POSITIVE_INFINITY : player.player.unit().maxHealth;
44 | }
45 |
46 | /** @return the new godmode status. */
47 | public boolean toggleGodmode(PlayerData player) {
48 | setGodmode(player, !player.inGodmode);
49 | return player.inGodmode;
50 | }
51 |
52 | @Override
53 | protected void initImpl() {
54 | // GodMode: instant build
55 | Vars.netServer.admins.addActionFilter(a -> {
56 | PlayerData p = PlayerData.get(a.player);
57 | if (p != null && p.inGodmode) {
58 | if (a.type == ActionType.placeBlock)
59 | ConstructBlock.constructed(a.tile, a.block, a.player.unit(), (byte)a.rotation, a.player.team(), a.config);
60 | else if (a.type == ActionType.breakBlock)
61 | Call.deconstructFinish(a.tile, a.block, a.player.unit());
62 | }
63 | return true;
64 | });
65 |
66 | // GodMode: infinite health
67 | Events.on(EventType.UnitChangeEvent.class, e -> {
68 | PlayerData player = PlayerData.get(e.player);
69 | if (player == null) return;
70 | if (player.lastUnit != null && player.lastUnit.health == Float.POSITIVE_INFINITY) player.lastUnit.clampHealth();
71 | if (player.inGodmode && !player.player.dead()) e.unit.health = Float.POSITIVE_INFINITY;
72 | player.lastUnit = e.unit;
73 | });
74 |
75 | // I need that to know when a player respawn to the core =/
76 | Events.run(EventType.Trigger.beforeGameUpdate, () ->
77 | PlayerData.each(p -> p.inGodmode && !p.player.dead(),
78 | p -> p.player.unit().health = Float.POSITIVE_INFINITY)
79 | );
80 |
81 | // GodMode: instant unit kill
82 | Events.on(EventType.UnitDamageEvent.class, e -> {
83 | if (e.unit.dead || !(e.bullet.owner instanceof Unitc) || e.bullet.owner == e.unit) return;
84 | PlayerData player = PlayerData.get(((Unitc)e.bullet.owner).getPlayer());
85 | if (player == null || !player.inGodmode) return;
86 | e.unit.kill();
87 | });
88 |
89 | // GodMode: instant block destroy
90 | Events.on(EventType.BuildDamageEvent.class, e -> {
91 | if (e.build.dead || !(e.source.shooter instanceof Unitc)) return;
92 | PlayerData player = PlayerData.get(((Unitc)e.source.shooter).getPlayer());
93 | if (player == null || !player.inGodmode) return;
94 | e.build.kill();
95 | });
96 | }
97 |
98 | @Override
99 | public void registerClientCommands(ClientCommandHandler handler) {
100 | handler.addAdmin("godmode", "[on|off] [player|selector...]", "[coral][[[scarlet]God[]]: [gold]I'm divine!",
101 | (args, player) -> {
102 | if (args.length == 0) {
103 | Players.info(player, "Godmode is currently [accent]@[].", player.inGodmode ? "enabled" : "disabled");
104 | return;
105 | }
106 |
107 | boolean enable;
108 | if (Strings.isTrue(args[0])) enable = true;
109 | else if (Strings.isFalse(args[0])) enable = false;
110 | else {
111 | Players.err(player, "Invalid argument! Must be 'on' or 'off'.");
112 | return;
113 | }
114 |
115 | if (args.length == 1) {
116 | if ((enable && player.inGodmode) || (!enable && !player.inGodmode))
117 | Players.err(player, "Godmode already [orange]@[].", enable ? "enabled" : "disabled");
118 | else {
119 | setGodmode(player, enable);
120 | Players.ok(player, "Godmode [accent]@[].", enable ? "enabled" : "disabled");
121 | }
122 | return;
123 | }
124 |
125 | SelectorParser selector = Modules.selector.parse(player, args, 1, args.length, true);
126 | if (selector == null) return;
127 | int[] count = {0};
128 | selector.execute((p, u) -> {
129 | if ((enable && p.inGodmode) || (!enable && !p.inGodmode))
130 | Players.warn(player, "Godmode already [accent]@[] for @[orange].", enable ? "enabled" : "disabled", p.getName());
131 | else {
132 | count[0]++;
133 | setGodmode(p, enable);
134 | if (p == player) return;
135 | Players.warn(p, "Godmode [accent]@[] by @[orange].", enable ? "enabled" : "disabled", player.getName());
136 | }
137 | });
138 | Players.ok(player, "@ godmode for [accent]@[] players.", enable ? "Enabled" : "Disabled", count[0]);
139 | });
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/src/main/java/fr/zetamap/morecommands/modules/tp/TeleportModule.java:
--------------------------------------------------------------------------------
1 | /**
2 | * This file is part of MoreCommands. The plugin that adds a bunch of commands to your server.
3 | * Copyright (c) 2025 ZetaMap
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | package fr.zetamap.morecommands.modules.tp;
20 |
21 | import arc.math.geom.Position;
22 |
23 | import mindustry.core.World;
24 | import mindustry.gen.Call;
25 | import mindustry.gen.Unit;
26 |
27 | import fr.zetamap.morecommands.Modules;
28 | import fr.zetamap.morecommands.PlayerData;
29 | import fr.zetamap.morecommands.command.ClientCommandHandler;
30 | import fr.zetamap.morecommands.misc.CoordinatesParser;
31 | import fr.zetamap.morecommands.misc.Players;
32 | import fr.zetamap.morecommands.module.AbstractModule;
33 | import fr.zetamap.morecommands.modules.selector.SelectorParser;
34 | import fr.zetamap.morecommands.modules.selector.Selectors;
35 |
36 |
37 | public class TeleportModule extends AbstractModule {
38 | public void teleport(PlayerData player, Position pos) {
39 | player.player.set(pos);
40 | player.player.snapInterpolation();
41 | if (!player.player.dead()) {
42 | player.player.unit().set(pos);
43 | player.player.unit().snapInterpolation();
44 | }
45 | Call.setPosition(player.player.con, pos.getX(), pos.getY());
46 | }
47 |
48 | public void teleport(Unit unit, Position pos) {
49 | unit.set(pos);
50 | unit.snapInterpolation();
51 | if (!unit.isPlayer()) return;
52 | unit.getPlayer().set(pos);
53 | unit.getPlayer().snapInterpolation();
54 | Call.setPosition(unit.getPlayer().con, pos.getX(), pos.getY());
55 | }
56 |
57 | @Override
58 | public void registerClientCommands(ClientCommandHandler handler) {
59 | handler.addAdmin("tp", " [player|dest-x,y...]", "Teleport to a location or player.",
60 | (args, player) -> {
61 | if (args[0].isEmpty()) Players.err(player, "Missing player, coordinates or selector.");
62 |
63 | else if (Selectors.isSelector(args[0])) {
64 | SelectorParser selector = Modules.selector.parse(player, args);
65 | if (selector == null) return; // Error already send to player
66 | CoordinatesParser dest = CoordinatesParser.parse(player, selector.rest);
67 | if (dest == null) return; // Error already send to player
68 |
69 | int tx = World.toTile(dest.pos.x), ty = World.toTile(dest.pos.y), x = (int)dest.pos.x, y = (int)dest.pos.y;
70 | selector.execute((p, u) -> {
71 | if (p != null) {
72 | teleport(p, dest.pos);
73 | if (p == player) return;
74 | if (dest.byCoordinates)
75 | Players.warn(p, "You have been teleported to [accent]@,@[] [gray]([lightgray]@[],[lightgray]@[])[] by @[orange].",
76 | tx, ty, x, y, player.getName());
77 | else Players.warn(p, "You have been teleported to @[orange] by @[orange].", dest.target.getName(), player.getName());
78 | } else teleport(u, dest.pos);
79 | });
80 | if (dest.byCoordinates)
81 | Players.ok(player, "@[green] to [accent]@,@[] [gray]([lightgray]@[],[lightgray]@[])[].",
82 | selector.formatMessage("Teleported", true), tx, ty, x, y);
83 | else Players.ok(player, "@[green] to @[green].", selector.formatMessage("Teleported", true), dest.target.getName());
84 |
85 | } else {
86 | CoordinatesParser src = CoordinatesParser.parse(player, args);
87 | if (src == null) return; // Error already send to player
88 | else if (src.byCoordinates && src.rest.length > 0)
89 | Players.err(player, "Too many arguments. "
90 | + "Usage: [orange]/tp [] or [orange]/tp [].");
91 |
92 | else if (src.rest.length > 0) {
93 | CoordinatesParser dest = CoordinatesParser.parse(player, src.rest);
94 | if (dest == null) return; // Error already send to player
95 |
96 | teleport(src.target, dest.pos);
97 | if (dest.byCoordinates) {
98 | int tx = World.toTile(dest.pos.x), ty = World.toTile(dest.pos.y), x = (int)dest.pos.x, y = (int)dest.pos.y;
99 | Players.ok(player, "Teleported @[green] to [accent]@,@[] [gray]([lightgray]@[],[lightgray]@[])[].",
100 | src.target.getName(), tx, ty, x, y);
101 | Players.warn(src.target, "You have been teleported to [accent]@,@[] [gray]([lightgray]@[],[lightgray]@[])[] by @[orange].",
102 | tx, ty, x, y, player.getName());
103 | } else {
104 | Players.ok(player, "Teleported @[green] to @[green].", src.target.getName(), dest.target.getName());
105 | Players.warn(src.target, "You have been teleported to @[orange] by @[orange].", dest.target.getName(),
106 | player.getName());
107 | }
108 |
109 | } else if (player.player.dead()) {
110 | Players.err(player, "Unable to find player position.");
111 | } else {
112 | teleport(player, src.pos);
113 | if (src.byCoordinates) {
114 | int tx = World.toTile(src.pos.x), ty = World.toTile(src.pos.y), x = (int)src.pos.x, y = (int)src.pos.y;
115 | Players.ok(player, "You teleported to @,@ [gray]([lightgray]@[],[lightgray]@[])[].", tx, ty, x, y);
116 | } else Players.ok(player, "You teleported to @[green].", src.target.getName());
117 | }
118 | }
119 | });
120 |
121 | //IDEA: /tpa, /tpahere, /tpaccept, /tpdeny.
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |  
2 |
3 | # More Commands plugin
4 | This plugin adds a bunch of commands (60+) to your server.
5 |
6 | > [!IMPORTANT]
7 | > This plugin requires Java 12 or higher.
8 | > To download Java 12, follow steps [here](https://www.oracle.com/fr/java/technologies/javase/jdk12-archive-downloads.html) or [here](https://www.oracle.com/java/technologies/downloads/) to download the latest version.
9 |
10 |
11 | ## Player commands *(total: 14)*
12 | * ``/help [command|page]`` - Lists all commands or selector examples.
13 | * ``/w `` - Whisper to a player.
14 | * ``/r `` - Reply to the last whispered message.
15 | * ``/votekick [player] [reason...]`` - Vote to kick a player with a valid reason.
16 | * ``/vote `` - Vote to kick the current player. Admins can cancel the vote with *'c'*.
17 | * ``/maps [page]`` - List all maps of the server.
18 | * ``/vnw [y|n|c|f|number]`` - Vote for sending a new wave.
19 | * ``/rtv [y|n|c|f|mapName...]`` - Vote to change the map.
20 | * ``/rainbow [on|off] [selector|player...]`` - RAINBOW!!
21 | * ``/effect [stop|list|search|name|id] [page|selector|player...]`` - Gives you a particle effect.
22 | * ``/hub`` - Connect you to the hub server. Shortcut of *'/switch hub'*.
23 | * ``/switch [name|alias...]`` - Connect you to another server.
24 | * ``/sync [selector|player...]`` - Re-synchronize world state of a player.
25 | * ``/pinfo [uuid|nickname...]`` - Get all player informations.
26 |
27 | ## Admin commands *(total: 25)*
28 | * ``/chat [on|off]`` - Toggle the chat.
29 | * ``/tag [page|set|remove] [UUID] [tag...]`` - Configure the tag system.
30 | * ``/team [teamName|vanish|~] [player|selector...]`` - Change team.
31 | * ``/tp [player|dest-x,y...]`` - Teleport to a location or player.
32 | * ``/place [player|x,y] [teamName|~] [buildData...]`` - Place a block.
33 | * ``/fill [teamName|~] [buildData...]`` - Fill a zone.
34 | * ``/core [player|x,y] [teamName|~...]`` - Build a core.
35 | * ``/spawn [count] [player|x,y] [teamName|~] [unitData...]`` - Spawn a unit.
36 | * ``/transform [player|selector] [unitData...]`` - Transform a player unit.
37 | * ``/kill [player|selector...]`` - Kill a player or a unit.
38 | * ``/clear-map [hard|y|n]`` - Kill all units and blocks, except cores, on the map.
39 | * ``/weather [clear|weatherName] [intensity] [inf|duration]`` - Control map weather.
40 | * ``/fillitems [team|all] [items...]`` - Fill the core of the specified or all teams, with the selected or all items.
41 | * ``/gamemode [name]`` - Change the current map gamemode.
42 | * ``/pause `` - Toggle the game state.
43 | * ``/godmode [on|off] [player|selector...]`` - **[God]:** I'm divine!
44 | * ``/ban [time] [reason...]`` - Ban a player.
45 | * ``/unban [reason...]`` - Unban a player.
46 | * ``/kick [time] [reason...]`` - Kick a player.
47 | * ``/warn `` - Warn a player.
48 | * ``/mute [time] [reason...]`` - Mute a player.
49 | * ``/unmute [reason...]`` - Unmute a player.
50 | * ``/freeze [time] [reason...]`` - Freeze a player.
51 | * ``/unfreeze [reason...]`` - Unfreeze a player.
52 | * ``/pardon [reason...]`` - Pardon a player or a punishment.
53 |
54 | ## Server commands *(total: 23)*
55 | * ``morecommands [on|off|reload|save] [moduleName]`` - Manage the MoreCommands modules. Requires a server restart to apply the changes.
56 | * ``commands [reset|on|off] [command] [now]`` - Toggle client/server commands.
57 | * ``chat [on|off]`` - Toggle the in-game chat.
58 | * ``tag [on|off|set|remove] [UUID] [tag...]`` - Configure the tag system.
59 | * ``effect [reset|id|name] [on|off|admins|everyone]`` - Manage the particles effects.
60 | * ``switch [help|arg0] [args...]`` - Configures the switch system. Use *'switch help'* for usage.
61 | * ``restart`` - Reconnects players and exits the server with code *'2'*, to ask a restart from the launcher script.
62 | * ``speed [value]`` - Control the game speed. **USE WITH CAUTION!**
63 | * ``fillitems [team|all] [items...]`` - Fill the core of the specified or all teams, with the selected or all items.
64 | * ``gamemode [name]`` - Change the current map gamemode.
65 | * ~~`blacklist [value...]` Players using a nickname or ip in the blacklist cannot connect.~~
66 | **Use [this plugin](https://github.com/xpdustry/simple-blacklist) instead. (the current configuration will be migrated automatically if the plugin is present)**
67 | * ~~`anti-vpn [on|off|token] [your_token]` Anti VPN service.~~
68 | **Use [this plugin](https://github.com/xpdustry/anti-vpn-service) instead. (the current configuration will be migrated automatically if the plugin is present)**
69 | * ``bans [all]`` - List all banned players and IPs.
70 | * ``ban [time] [reason...]`` - Ban a player.
71 | * ``unban [reason...]`` - Unban a player.
72 | * ``kick [time] [reason...]`` - Kick a player.
73 | * ``warn `` - Warn a player.
74 | * ``mute [time] [reason...]`` - Mute a player.
75 | * ``unmute [reason...]`` - Unmute a player.
76 | * ``freeze [time] [reason...]`` - Freeze a player.
77 | * ``unfreeze [reason...]`` - Unfreeze a player.
78 | * ``pardon [reason...]`` - Pardon a player or a punishment.
79 | * ``punishments [type|uuid|ip] [all|type]`` - View all or a player punishments.
80 | * ``reserved-names [on|off|add|remove|message] [name|text...]`` - Reserved nicknames can only be used by admins.
81 | * ``anti-evade [clear|minutes]`` - Control the anti evade system.
82 |
83 |
84 | ## Building
85 | Pre-build releases can be found [here](https://github.com/ZetaMap/moreCommands/releases).
86 | Or you can build it yourself by running ``./gradlew build``. The file will be named ``more-commands.jar``
87 |
88 |
89 | ## Documentation
90 | ### Selectors
91 | **TODO**
92 |
--------------------------------------------------------------------------------
/src/main/java/fr/zetamap/morecommands/modules/voting/VoteNewWaveSession.java:
--------------------------------------------------------------------------------
1 | /**
2 | * This file is part of MoreCommands. The plugin that adds a bunch of commands to your server.
3 | * Copyright (c) 2025 ZetaMap
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | package fr.zetamap.morecommands.modules.voting;
20 |
21 | import mindustry.Vars;
22 | import mindustry.gen.Call;
23 |
24 | import fr.zetamap.morecommands.PlayerData;
25 | import fr.zetamap.morecommands.misc.Players;
26 | import fr.zetamap.morecommands.util.DurationFormatter;
27 | import fr.zetamap.morecommands.util.Strings;
28 |
29 |
30 | public class VoteNewWaveSession extends PlayerVoteSession {
31 | public VoteNewWaveSession() {
32 | super(1* 60, 2 * 60);
33 | }
34 |
35 | public boolean canStart(PlayerData player, Integer wave) {
36 | if (PlayerData.size() < 3 && !player.admin()) {
37 | Players.err(player, "At least 3 players are required to start a vote.");
38 | return false;
39 | } else if (started()) {
40 | Players.err(player, "A vote to run [orange]@[] is already in progress!\n"
41 | + "Type [orange]/vnw y[] or [orange]/vnw n[] to agree or not.", stringObjective());
42 | return false;
43 | } else if (waitRemaining() > 0) {
44 | Players.err(player, "You must wait [orange]@[] before able to restart a vote.",
45 | DurationFormatter.format(waitRemaining()));
46 | return false;
47 | } else if (wave < 1) {
48 | Players.err(player, "Invalid number of wave. Must be greater than [orange]1[].");
49 | return false;
50 | }
51 | return true;
52 | }
53 |
54 | /** Start a new vote session to skip one wave. */
55 | public boolean start(PlayerData player) {
56 | return start(player, 1);
57 | }
58 |
59 | public boolean canVote(PlayerData player) {
60 | if (!started()) {
61 | Players.err(player, "No vote session in progress.");
62 | return false;
63 | } else if (voted(player) != null) {
64 | Players.info(player, "You already voted to run [accent]@[].", stringObjective());
65 | return false;
66 | }
67 | return true;
68 | }
69 |
70 | public boolean canStop(PlayerData player) {
71 | if (!started()) {
72 | Players.err(player, "No vote session in progress.");
73 | return false;
74 | } else if (!player.admin()) {
75 | Players.errArgUseDenied(player);
76 | return false;
77 | }
78 | return true;
79 | }
80 |
81 | @Override
82 | public int required() {
83 | return PlayerData.size() / 2 + 1;
84 | }
85 |
86 | /**
87 | * Skip the waves.
88 | * Note that doesn't just increases the wave number, it runs them.
89 | */
90 | public void skipWaves() {
91 | int waves = objective();
92 | while (waves-- > 0) Vars.logic.skipWave();
93 | }
94 |
95 | @Override
96 | protected void sessionStarted(PlayerData by) {
97 | int remaining = required() - votes();
98 | String vote = remaining == 1 ? "vote is" : "votes are";
99 | Call.sendMessage(
100 | Strings.format("[scarlet]VNW: @ [white] started a vote to run [accent]@[].\n"
101 | + "[scarlet]VNW: [accent]@[white] more @ required [gray]([lightgray]@[]/[lightgray]@[])[]. "
102 | + "Type [orange]/vnw y[] or [orange]/vnw n[] to agree or not.",
103 | by.getName(), stringObjective(), remaining, vote, votes(), required()));
104 | }
105 |
106 | @Override
107 | protected void sessionPassed() {
108 | Call.sendMessage(Strings.format("[scarlet]VNW:[green] Vote passed, [accent]@[] will start soon.", stringObjective()));
109 | skipWaves();
110 | }
111 |
112 | @Override
113 | protected void sessionForced(PlayerData by) {
114 | Call.sendMessage(Strings.format("[scarlet]VNW:[green] Vote skipped by @[green], [accent]@[] will start soon.",
115 | by.getName(), stringObjective()));
116 | skipWaves();
117 | }
118 |
119 | @Override
120 | protected void sessionFailed() {
121 | Call.sendMessage(Strings.format("[scarlet]VNW: Vote failed![] Not enough votes to run [accent]@[].", stringObjective()));
122 | }
123 |
124 | @Override
125 | protected void sessionCanceled(PlayerData by) {
126 | Call.sendMessage(Strings.format("[scarlet]VNW:[orange] Vote cancelled by @[orange].", by.getName()));
127 | }
128 |
129 | @Override
130 | protected void sessionVote(PlayerData who, VoteType type) {
131 | int remaining = required() - votes();
132 | String vote = remaining == 1 ? "vote is" : "votes are";
133 | Call.sendMessage(
134 | Strings.format("[scarlet]VNW: @ [white] voted to @run [accent]@[white].\n"
135 | + "[scarlet]VNW: [accent]@[white] more @ required [gray]([lightgray]@[]/[lightgray]@[])[]."
136 | + "Type [orange]/vnw y[] or [orange]/vnw n[] to agree or not with him.",
137 | who.getName(), type.yes() ? "" : "not ", stringObjective(), remaining, vote, votes(), required()));
138 | }
139 |
140 | @Override
141 | protected void sessionVoteRemoved(PlayerData who) {
142 | int remaining = required() - votes();
143 | String vote = remaining == 1 ? "vote is" : "votes are";
144 | Call.sendMessage(Strings.format("[scarlet]VNW: @ [orange]left the game, [accent]@[white] more @ now required "
145 | + "[gray]([lightgray]@[]/[lightgray]@[])[].",
146 | who.getName(), remaining, vote, votes(), required()));
147 | }
148 |
149 | protected String stringObjective() {
150 | int waves = objective();
151 | return (waves == 1 ? "the" : waves+"") + ' ' + (waves == 1 ? "wave" : "waves");
152 | }
153 | }
154 |
--------------------------------------------------------------------------------
/src/main/java/fr/zetamap/morecommands/modules/manager/ManagerModule.java:
--------------------------------------------------------------------------------
1 | /**
2 | * This file is part of MoreCommands. The plugin that adds a bunch of commands to your server.
3 | * Copyright (c) 2025 ZetaMap
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | package fr.zetamap.morecommands.modules.manager;
20 |
21 | import arc.struct.ObjectSet;
22 | import arc.struct.Seq;
23 |
24 | import fr.zetamap.morecommands.command.ServerCommandHandler;
25 | import fr.zetamap.morecommands.module.AbstractModule;
26 | import fr.zetamap.morecommands.module.Module;
27 | import fr.zetamap.morecommands.module.ModuleFactory;
28 | import fr.zetamap.morecommands.util.Autosaver;
29 | import fr.zetamap.morecommands.util.Strings;
30 |
31 |
32 | /** MoreCommands manager module. */
33 | public class ManagerModule extends AbstractModule {
34 | public final ObjectSet protectedModules = ObjectSet.with("manager", "commands");
35 |
36 | public boolean enabled(Module module) {
37 | return ModuleFactory.enabled(module);
38 | }
39 |
40 | public void enable(Module module) {
41 | if (isProtected(module)) return;
42 | ModuleFactory.enable(module);
43 | }
44 |
45 | public void disable(Module module) {
46 | if (isProtected(module)) return;
47 | ModuleFactory.disable(module);
48 | }
49 |
50 | public boolean isProtected(Module module) {
51 | return protectedModules.contains(module.internalName());
52 | }
53 |
54 | @Override
55 | protected void initImpl() {
56 | //TODO: find a way to ensure that protected modules are not disabled
57 | protectedModules.each(n -> {
58 | Module m = ModuleFactory.get(n);
59 | if (m != null) ModuleFactory.enable(m);
60 | });
61 | }
62 |
63 | @Override
64 | public void registerServerCommands(ServerCommandHandler handler) {
65 | handler.add("morecommands", "[on|off|reload|save] [moduleName]",
66 | "Manage the MoreCommands modules. Requires a server restart to apply the changes.", args -> {
67 | int action = 0; // 0: enabled, 1: disable, 2: reload
68 | if (args.length == 0) {
69 | logger.info("MoreCommands modules: [total: @, enabled: @, saveable: @]", ModuleFactory.size(),
70 | ModuleFactory.count(ModuleFactory::enabled), ModuleFactory.count(ModuleFactory::saveable));
71 | Seq left = new Seq<>(ModuleFactory.size()), right = new Seq<>(ModuleFactory.size());
72 | ModuleFactory.each(m -> left.add("&lk|&fr &fb&lb" + m.internalName() + "&fr (&fb&lb" + m.name() + "&fr):"));
73 | ModuleFactory.each(m -> right.add((protectedModules.contains(m.internalName()) ? "&lyprotected&fr" :
74 | ModuleFactory.enabled(m) ? "&lgenabled&fr" : "&lrdisabled&fr") +
75 | (ModuleFactory.saveable(m) ? ", &lbsaveable&fr" : "")));
76 | Strings.sJust(Strings.lJust(left, Strings.maxLength(left) + 1), right, 0).each(logger::info);
77 | return;
78 | }
79 | else if (Strings.isTrue(args[0], false)) action = 0;
80 | else if (Strings.isFalse(args[0], false)) action = 1;
81 | else if (args[0].equals("reload")) action = 2;
82 | else if (args[0].equals("save")) {
83 | if (ModuleFactory.disposed()) logger.err("Cannot save: factory is disposed!");
84 | else if (!ModuleFactory.initialized()) logger.err("Cannot save: factory is not initialized!");
85 | else if (ModuleFactory.save()) {
86 | logger.info("MoreCommands modules saved.");
87 | Autosaver.save();
88 | }
89 | // error already printed if error
90 | return;
91 |
92 | } else {
93 | logger.err("Invalid argument! Must be 'on', 'off', 'reload' or 'save'.");
94 | return;
95 | }
96 |
97 | if (args.length == 1) {
98 | if (action < 2) logger.err("Missing 'moduleName' argument.");
99 | else if (ModuleFactory.disposed()) logger.err("Cannot reload: factory is disposed!");
100 | else if (!ModuleFactory.initialized()) logger.err("Cannot reload: factory is not initialized!");
101 | else if (ModuleFactory.reload()) logger.info("MoreCommands modules reloaded.");
102 | // error already printed if error
103 | return;
104 | }
105 |
106 | Module module = ModuleFactory.get(args[1]);
107 | if (module == null) {
108 | logger.err("No module named '@' found.", args[1]);
109 | } else if (action == 0) {
110 | if (ModuleFactory.enabled(module)) logger.err("Module already enabled.");
111 | else {
112 | ModuleFactory.enable(module);
113 | logger.info("Module enabled.");
114 | logger.warn("A server restart is required to apply the change.");
115 | }
116 | } else if (action == 1) {
117 | if (isProtected(module)) logger.err("This module is protected, you cannot disable it.");
118 | else if (!ModuleFactory.enabled(module)) logger.err("Module already disabled.");
119 | else {
120 | ModuleFactory.disable(module);
121 | logger.info("Module disabled.");
122 | logger.warn("A server restart is required to apply the change.");
123 | }
124 | }
125 | else if (!ModuleFactory.saveable(module)) logger.warn("This module has no settings support.");
126 | else if (!ModuleFactory.enabled(module)) logger.warn("The module is disabled. Please enable it before reloading.");
127 | else if (ModuleFactory.disposed()) logger.err("Cannot reload module @: factory is disposed!", module.internalName());
128 | else if (!ModuleFactory.initialized())
129 | logger.err("Cannot reload module @: factory is not initialized!", module.internalName());
130 | else {
131 | try {
132 | ModuleFactory.reload(module);
133 | logger.info("Module settings reloaded.");
134 | } catch (Exception e) { logger.err("Failed to reload @ settings" , e, module.name()); }
135 | }
136 | });
137 | }
138 | }
139 |
--------------------------------------------------------------------------------
/src/main/java/fr/zetamap/morecommands/util/Logger.java:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Anti-VPN-Service (AVS). The plugin securing your server against VPNs.
3 | *
4 | * MIT License
5 | *
6 | * Copyright (c) 2024-2025 Xpdustry
7 | *
8 | * Permission is hereby granted, free of charge, to any person obtaining a copy
9 | * of this software and associated documentation files (the "Software"), to deal
10 | * in the Software without restriction, including without limitation the rights
11 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 | * copies of the Software, and to permit persons to whom the Software is
13 | * furnished to do so, subject to the following conditions:
14 | *
15 | * The above copyright notice and this permission notice shall be included in all
16 | * copies or substantial portions of the Software.
17 | *
18 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24 | * SOFTWARE.
25 | */
26 |
27 | package fr.zetamap.morecommands.util;
28 |
29 | import arc.util.Log;
30 | import arc.util.Log.LogLevel;
31 |
32 | import mindustry.Vars;
33 | import mindustry.mod.Mods;
34 | import mindustry.mod.Mod;
35 |
36 |
37 | /** Log messages to console with topics */
38 | public class Logger {
39 | protected static final Object[] empty = {};
40 | /** Will use slf4j when slf4md plugin is present */
41 | protected static boolean slf4mdPresent;
42 | protected static Object slf4jLogger;
43 |
44 | public static String mainTopic;
45 | public static String topicFormat = "&ly[@&ly]";
46 |
47 | /** Sets the main topic using the mod. */
48 | public static void init(Mod mod) {
49 | init(mod.getClass());
50 | }
51 |
52 | /** Sets the main topic using the mod class. */
53 | public static void init(Class extends Mod> mod) {
54 | Mods.LoadedMod load = Vars.mods.getMod(mod);
55 | if (load == null) throw new IllegalArgumentException("Mod is not loaded yet (or missing)!");
56 | init(load.meta.displayName);
57 | }
58 |
59 | /** Sets the main topic */
60 | public static void init(String mainTopic) {
61 | Logger.mainTopic = "&lc[" + mainTopic + "&lc]";
62 | slf4mdPresent = Vars.mods.locateMod("slf4md") != null;
63 | if (slf4mdPresent) slf4jLogger = org.slf4j.LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME);
64 | }
65 |
66 |
67 | /** If {@code true}, will not print the plugin and logger topics. */
68 | public boolean noTopic = false;
69 | protected String topic, ftopic;
70 |
71 | /** Will only use the {@link #mainTopic}. */
72 | public Logger() {
73 | this((String)null);
74 | }
75 |
76 | public Logger(boolean noTopic) {
77 | this.noTopic = noTopic;
78 | }
79 |
80 | public Logger(Class> clazz) {
81 | this(Strings.insertSpaces(clazz.getSimpleName()));
82 | }
83 |
84 | public Logger(String topic) {
85 | if (topic != null && !(topic = topic.trim()).isEmpty())
86 | topic(topic);
87 | }
88 |
89 | public String topic() {
90 | return topic;
91 | }
92 |
93 | public void topic(String topic) {
94 | this.topic = topic;
95 | this.ftopic = topic == null ? null : Strings.format(topicFormat, topic);
96 | }
97 |
98 | public synchronized void log(LogLevel level, String text, Throwable th, Object... args) {
99 | if (Log.level.ordinal() > level.ordinal()) return;
100 |
101 | String tag = noTopic ? "" : Log.format((mainTopic != null ? mainTopic + " " : "") +
102 | (ftopic != null ? ftopic + "&fr " : "&fr"), empty);
103 |
104 | if (text != null) {
105 | text = Log.format(text, args);
106 | if (th != null) text += ": " + Strings.getStackTrace(th);
107 | } else if (th != null) text = Strings.getStackTrace(th);
108 |
109 | if (slf4mdPresent && slf4jLogger != null) {
110 | synchronized (slf4jLogger) {
111 | org.slf4j.Logger l = (org.slf4j.Logger)slf4jLogger;
112 | arc.func.Cons printer;
113 |
114 | switch (level) {
115 | case debug: printer = l::debug; break;
116 | case info: printer = l::info; break;
117 | case warn: printer = l::warn; break;
118 | case err: printer = l::error; break;
119 | default: return;
120 | }
121 |
122 | if (text == null || text.isEmpty()) {
123 | printer.get(tag);
124 | return;
125 | }
126 |
127 | int i = 0, nl = text.indexOf('\n');
128 | while (nl >= 0) {
129 | printer.get(tag + text.substring(i, nl));
130 | i = nl + 1;
131 | nl = text.indexOf('\n', i);
132 | }
133 | printer.get(tag + (i == 0 ? text : text.substring(i)));
134 | }
135 |
136 | } else if (text == null || text.isEmpty()) {
137 | synchronized (Log.logger) {
138 | Log.logger.log(level, tag);
139 | }
140 |
141 | } else {
142 | synchronized (Log.logger) {
143 | int i = 0, nl = text.indexOf('\n');
144 | while (nl >= 0) {
145 | Log.logger.log(level, tag + text.substring(i, nl));
146 | i = nl + 1;
147 | nl = text.indexOf('\n', i);
148 | }
149 | Log.logger.log(level, tag + (i == 0 ? text : text.substring(i)));
150 | }
151 | }
152 | }
153 | public void log(LogLevel level, String text, Object... args) { log(level, text, null, args); }
154 | public void log(LogLevel level, String text) { log(level, text, empty); }
155 |
156 | public void debug(String text, Object... args) { log(LogLevel.debug, text, args); }
157 | public void debug(Object object) { debug(String.valueOf(object), empty); }
158 |
159 | public void info(String text, Object... args) { log(LogLevel.info, text, args); }
160 | public void info(Object object) { info(String.valueOf(object), empty); }
161 |
162 | public void warn(String text, Object... args) { log(LogLevel.warn, text, args); }
163 | public void warn(String text) { warn(text, empty); }
164 |
165 | public void err(String text, Throwable th, Object... args) { log(LogLevel.err, text, th, args); }
166 | public void err(String text, Object... args) { err(text, null, args); }
167 | public void err(String text, Throwable th) { err(text, th, empty); }
168 | public void err(String text) { err(text, null, empty); }
169 | public void err(Throwable th) { err(null, th, empty); }
170 |
171 | /** Log an empty "info" line. */
172 | public void ln() { log(LogLevel.info, null, empty); }
173 | }
174 |
--------------------------------------------------------------------------------