package carpet.settings; import carpet.CarpetServer; import carpet.utils.Messenger; import com.google.common.collect.ImmutableList; import com.mojang.brigadier.CommandDispatcher; import com.mojang.brigadier.arguments.StringArgumentType; import com.mojang.brigadier.builder.LiteralArgumentBuilder; import com.mojang.brigadier.context.CommandContext; import com.mojang.brigadier.exceptions.CommandSyntaxException; import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; import net.minecraft.entity.player.PlayerEntity; import net.minecraft.server.MinecraftServer; import net.minecraft.server.ServerTask; import net.minecraft.server.command.ServerCommandSource; import net.minecraft.server.network.ServerPlayerEntity; import net.minecraft.text.BaseText; import org.apache.commons.lang3.tuple.Pair; import org.apache.logging.log4j.util.TriConsumer; import java.io.BufferedReader; import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.io.PrintStream; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.stream.Collectors; import static net.minecraft.server.command.CommandManager.argument; import static net.minecraft.server.command.CommandManager.literal; import static net.minecraft.server.command.CommandSource.suggestMatching; public class SettingsManager { private Map> rules = new HashMap<>(); public boolean locked; private final String version; private final String identifier; private final String fancyName; private MinecraftServer server; private List, String>> observers = new ArrayList<>(); public SettingsManager(String version, String identifier) { this.version = version; this.identifier = identifier; this.fancyName = identifier; } public SettingsManager(String version, String identifier, String fancyName) { this.version = version; this.identifier = identifier; this.fancyName = fancyName; } public void attachServer(MinecraftServer server) { this.server = server; loadConfigurationFromConf(); registerCommand(server.getCommandManager().getDispatcher()); notifyPlayersCommandsChanged(); } public void parseSettingsClass(Class settingsClass) { for (Field f : settingsClass.getDeclaredFields()) { Rule rule = f.getAnnotation(Rule.class); if (rule == null) continue; ParsedRule parsed = new ParsedRule(f, rule); rules.put(parsed.name, parsed); } } public void addRuleObserver(TriConsumer, String> observer) { observers.add(observer); } void notifyRuleChanged(ServerCommandSource source, ParsedRule rule, String userTypedValue) { observers.forEach(observer -> observer.accept(source, rule, userTypedValue)); } public Iterable getCategories() { Set categories = new HashSet<>(); getRules().stream().map(r -> r.categories).forEach(categories::addAll); return categories; } public ParsedRule getRule(String name) { return rules.get(name); } public Collection> getRules() { return rules.values().stream().sorted().collect(Collectors.toList()); } public Collection> findStartupOverrides() { Set defaults = readSettingsFromConf().getLeft().keySet(); return rules.values().stream().filter(r -> defaults.contains(r.name)). sorted().collect(Collectors.toList()); } public Collection> getNonDefault() { return rules.values().stream().filter(r -> !r.isDefault()).sorted().collect(Collectors.toList()); } private File getFile() { return server.getLevelStorage().resolveFile(server.getLevelName(), identifier+".conf"); } public void disableBooleanFromCategory(String category) { for (ParsedRule rule : rules.values()) { if (rule.type != boolean.class || !rule.categories.contains(category)) continue; ((ParsedRule) rule).set(server.getCommandSource(), false, "false"); } } private void writeSettingsToConf(Map values) { if (locked) return; try { FileWriter fw = new FileWriter(getFile()); for (String key: values.keySet()) { fw.write(key+" "+values.get(key)+"\n"); } fw.close(); } catch (IOException e) { e.printStackTrace(); CarpetSettings.LOG.error("[CM]: failed write "+identifier+".conf config file"); } ///todo is it really needed? resendCommandTree(); } public void notifyPlayersCommandsChanged() { if (server.getPlayerManager() == null) { return; } server.method_18858(new ServerTask(this.server.getTicks(), () -> { for (ServerPlayerEntity entityplayermp : server.getPlayerManager().getPlayerList()) { server.getCommandManager().sendCommandTree(entityplayermp); } })); } private void loadConfigurationFromConf() { for (ParsedRule rule : rules.values()) rule.resetToDefault(server.getCommandSource()); Pair,Boolean> conf = readSettingsFromConf(); locked = false; if (conf.getRight()) { CarpetSettings.LOG.info("[CM]: "+fancyName+" features are locked by the administrator"); disableBooleanFromCategory(RuleCategory.COMMAND); } for (String key: conf.getLeft().keySet()) { if (rules.get(key).set(server.getCommandSource(), conf.getLeft().get(key)) != null) CarpetSettings.LOG.info("[CM]: loaded setting "+key+" as "+conf.getLeft().get(key)+" from "+identifier+".conf"); } locked = conf.getRight(); } private Pair,Boolean> readSettingsFromConf() { try { BufferedReader reader = new BufferedReader(new FileReader(getFile())); String line = ""; boolean confLocked = false; Map result = new HashMap(); while ((line = reader.readLine()) != null) { line = line.replaceAll("\\r|\\n", ""); if ("locked".equalsIgnoreCase(line)) { confLocked = true; } String[] fields = line.split("\\s+",2); if (fields.length > 1) { if (!rules.containsKey(fields[0])) { CarpetSettings.LOG.error("[CM]: "+fancyName+" Setting " + fields[0] + " is not a valid - ignoring..."); continue; } ParsedRule rule = rules.get(fields[0]); if (!(rule.options.contains(fields[1])) && rule.isStrict) { CarpetSettings.LOG.error("[CM]: The value of " + fields[1] + " for " + fields[0] + "("+fancyName+") is not valid - ignoring..."); continue; } result.put(fields[0],fields[1]); } } reader.close(); return Pair.of(result, confLocked); } catch(FileNotFoundException e) { return Pair.of(new HashMap<>(), false); } catch (IOException e) { e.printStackTrace(); return Pair.of(new HashMap<>(), false); } } public Collection> getRulesMatching(String search) { String lcSearch = search.toLowerCase(Locale.ROOT); return rules.values().stream().filter(rule -> { if (rule.name.toLowerCase(Locale.ROOT).contains(lcSearch)) return true; for (String c : rule.categories) if (c.toLowerCase(Locale.ROOT).equals(search)) return true; return false; }).collect(ImmutableList.toImmutableList()); } public int printAllRulesToLog() { PrintStream ps = System.out; ps.println("# "+fancyName+" Settings"); for (Map.Entry> e : new TreeMap<>(rules).entrySet()) { ParsedRule rule = e.getValue(); ps.println("## " + rule.name); ps.println(rule.description+" "); for (String extra : rule.extraInfo) ps.println(extra + " "); ps.println("* Type: `" + rule.type.getSimpleName() + "` "); ps.println("* Default value: `" + rule.defaultAsString + "` "); String optionString = rule.options.stream().map(s -> "`" + s + "`").collect(Collectors.joining(", ")); ps.println((rule.isStrict?"* Required":"* Suggested")+" options: " + optionString + " "); ps.println("* Categories: " + rule.categories.stream().map(s -> "`" + s.toUpperCase(Locale.ROOT) + "`").collect(Collectors.joining(", ")) + " "); boolean preamble = false; for (Validator validator : rule.validators) { if(validator.description() != null) { if (!preamble) { ps.println("* Additional notes: "); preamble = true; } ps.println(" * "+validator.description()+" "); } } ps.println(" "); } return 1; } private ParsedRule contextRule(CommandContext ctx) throws CommandSyntaxException { String ruleName = StringArgumentType.getString(ctx, "rule"); ParsedRule rule = getRule(ruleName); if (rule == null) throw new SimpleCommandExceptionType(Messenger.c("rb Unknown rule: "+ruleName)).create(); return rule; } private void registerCommand(CommandDispatcher dispatcher) { if (dispatcher.getRoot().getChildren().stream().anyMatch(node -> node.getName().equalsIgnoreCase(identifier))) { Messenger.print_server_message(CarpetServer.minecraft_server, Messenger.c("rb Failed to add settings command for " + identifier + ". It is masking previous command.")); return; } LiteralArgumentBuilder literalargumentbuilder = literal(identifier).requires((player) -> player.hasPermissionLevel(2) && !locked); literalargumentbuilder.executes((context)->listAllSettings(context.getSource())). then(literal("list"). executes( (c) -> listSettings(c.getSource(), "All "+fancyName+" Settings", getRules())). then(literal("defaults"). executes( (c)-> listSettings(c.getSource(), "Current "+fancyName+" Startup Settings from "+identifier+".conf", findStartupOverrides()))). then(argument("tag",StringArgumentType.word()). suggests( (c, b)->suggestMatching(getCategories(), b)). executes( (c) -> listSettings(c.getSource(), String.format(fancyName+" Settings matching \"%s\"", StringArgumentType.getString(c, "tag")), getRulesMatching(StringArgumentType.getString(c, "tag")))))). then(literal("removeDefault"). requires(s -> !locked). then(argument("rule", StringArgumentType.word()). suggests( (c, b) -> suggestMatching(getRules().stream().map(r -> r.name), b)). executes((c) -> removeDefault(c.getSource(), contextRule(c))))). then(literal("setDefault"). requires(s -> !locked). then(argument("rule", StringArgumentType.word()). suggests( (c, b) -> suggestMatching(getRules().stream().map(r -> r.name), b)). then(argument("value", StringArgumentType.word()). suggests((c, b)-> suggestMatching(contextRule(c).options, b)). executes((c) -> setDefault(c.getSource(), contextRule(c), StringArgumentType.getString(c, "value")))))). then(argument("rule", StringArgumentType.word()). suggests( (c, b) -> suggestMatching(getRules().stream().map(r -> r.name), b)). requires(s -> !locked ). executes( (c) -> displayRuleMenu(c.getSource(), contextRule(c))). then(argument("value", StringArgumentType.word()). suggests((c, b)-> suggestMatching(contextRule(c).options,b)). executes((c) -> setRule(c.getSource(), contextRule(c), StringArgumentType.getString(c, "value"))))); dispatcher.register(literalargumentbuilder); } private int displayRuleMenu(ServerCommandSource source, ParsedRule rule) { PlayerEntity player; try { player = source.getPlayer(); } catch (CommandSyntaxException e) { Messenger.m(source, "w "+rule.name +" is set to: ","wb "+rule.getAsString()); return 1; } Messenger.m(player, ""); Messenger.m(player, "wb "+rule.name,"!/"+identifier+" "+rule.name,"^g refresh"); Messenger.m(player, "w "+rule.description); rule.extraInfo.forEach(s -> Messenger.m(player, "g "+s)); List tags = new ArrayList<>(); tags.add(Messenger.c("w Tags: ")); for (String t: rule.categories) { tags.add(Messenger.c("c ["+t+"]", "^g list all "+t+" settings","!/"+identifier+" list "+t)); tags.add(Messenger.c("w , ")); } tags.remove(tags.size()-1); Messenger.m(player, tags.toArray(new Object[0])); Messenger.m(player, "w Current value: ",String.format("%s %s (%s value)",rule.getBoolValue()?"lb":"nb", rule.getAsString(),rule.isDefault()?"default":"modified")); List options = new ArrayList<>(); options.add(Messenger.c("w Options: ", "y [ ")); for (String o: rule.options) { options.add(makeSetRuleButton(rule, o, false)); options.add(Messenger.c("w ")); } options.remove(options.size()-1); options.add(Messenger.c("y ]")); Messenger.m(player, options.toArray(new Object[0])); return 1; } private int setRule(ServerCommandSource source, ParsedRule rule, String newValue) { if (rule.set(source, newValue) != null) Messenger.m(source, "w "+rule.toString()+", ", "c [change permanently?]", "^w Click to keep the settings in carpet.conf to save across restarts", "?/carpet setDefault "+rule.name+" "+rule.getAsString()); return 1; } // stores different defaults in the file private int setDefault(ServerCommandSource source, ParsedRule rule, String stringValue) { if (locked) return 0; if (!rules.containsKey(rule.name)) return 0; Pair,Boolean> conf = readSettingsFromConf(); conf.getLeft().put(rule.name, stringValue); writeSettingsToConf(conf.getLeft()); // this may feels weird, but if conf // is locked, it will never reach this point. rule.set(source,stringValue); Messenger.m(source ,"gi rule "+ rule.name+" will now default to "+ stringValue); return 1; } // removes overrides of the default values in the file private int removeDefault(ServerCommandSource source, ParsedRule rule) { if (locked) return 0; if (!rules.containsKey(rule.name)) return 0; Pair,Boolean> conf = readSettingsFromConf(); conf.getLeft().remove(rule.name); writeSettingsToConf(conf.getLeft()); rules.get(rule.name).resetToDefault(source); return 1; } private BaseText displayInteractiveSetting(ParsedRule rule) { List args = new ArrayList<>(); args.add("w - "+rule.name+" "); args.add("!/"+identifier+" "+rule.name); args.add("^y "+rule.description); for (String option: rule.options) { args.add(makeSetRuleButton(rule, option, true)); args.add("w "); } if (!rule.options.contains(rule.getAsString())) { args.add(makeSetRuleButton(rule, rule.getAsString(), true)); args.add("w "); } args.remove(args.size()-1); return Messenger.c(args.toArray(new Object[0])); } private BaseText makeSetRuleButton(ParsedRule rule, String option, boolean brackets) { String style = rule.isDefault()?"g":(option.equalsIgnoreCase(rule.defaultAsString)?"e":"y"); if (option.equalsIgnoreCase(rule.getAsString())) { style = style + "u"; if (option.equalsIgnoreCase(rule.defaultAsString)) style = style + "b"; } String baseText = style + (brackets ? " [" : " ") + option + (brackets ? "]" : ""); if (locked) return Messenger.c(baseText, "^g "+fancyName+" settings are locked"); if (option.equalsIgnoreCase(rule.getAsString())) return Messenger.c(baseText); return Messenger.c(baseText, "^g Switch to " + option+(option.equalsIgnoreCase(rule.defaultAsString)?" (default)":""), "?/"+identifier+" " + rule.name + " " + option); } private int listSettings(ServerCommandSource source, String title, Collection> settings_list) { try { PlayerEntity player = source.getPlayer(); Messenger.m(player,String.format("wb %s:",title)); settings_list.forEach(e -> Messenger.m(player,displayInteractiveSetting(e))); } catch (CommandSyntaxException e) { Messenger.m(source, "w s:"+title); settings_list.forEach(r -> Messenger.m(source, "w - "+ r.toString())); } return 1; } private int listAllSettings(ServerCommandSource source) { listSettings(source, "Current "+fancyName+" Settings", getNonDefault()); if (version != null) Messenger.m(source, fancyName+" version: "+ version); try { PlayerEntity player = source.getPlayer(); List tags = new ArrayList<>(); tags.add("w Browse Categories:\n"); for (String t : getCategories()) { tags.add("c [" + t+"]"); tags.add("^g list all " + t + " settings"); tags.add("!/"+identifier+" list " + t); tags.add("w "); } tags.remove(tags.size() - 1); Messenger.m(player, tags.toArray(new Object[0])); } catch (CommandSyntaxException ignored) { } return 1; } }