Files
fabric-carpet/src/main/java/carpet/settings/SettingsManager.java
T

525 lines
20 KiB
Java

package carpet.settings;
import carpet.CarpetServer;
import carpet.utils.Messenger;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Sets;
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<String, ParsedRule<?>> rules = new HashMap<>();
public boolean locked;
private final String version;
private final String identifier;
private final String fancyName;
private MinecraftServer server;
private List<TriConsumer<ServerCommandSource, ParsedRule<?>, 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 detachServer()
{
for (ParsedRule<?> rule : rules.values()) rule.resetToDefault(server.getCommandSource());
server = null;
}
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<ServerCommandSource, ParsedRule<?>, String> observer)
{
observers.add(observer);
}
void notifyRuleChanged(ServerCommandSource source, ParsedRule<?> rule, String userTypedValue)
{
observers.forEach(observer -> observer.accept(source, rule, userTypedValue));
}
public Iterable<String> getCategories()
{
Set<String> 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<ParsedRule<?>> getRules()
{
return rules.values().stream().sorted().collect(Collectors.toList());
}
public Collection<ParsedRule<?>> findStartupOverrides()
{
Set<String> defaults = readSettingsFromConf().getLeft().keySet();
return rules.values().stream().filter(r -> defaults.contains(r.name)).
sorted().collect(Collectors.toList());
}
public Collection<ParsedRule<?>> 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<Boolean>) rule).set(server.getCommandSource(), false, "false");
}
}
private void writeSettingsToConf(Map<String, String> 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 == null || server.getPlayerManager() == null)
{
return;
}
server.send(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<Map<String, String>,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())
{
try
{
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");
}
catch (Exception exc)
{
CarpetSettings.LOG.error("[CM Error]: Failed to load setting: "+key+", "+exc);
}
}
locked = conf.getRight();
}
private Pair<Map<String, String>,Boolean> readSettingsFromConf()
{
try
{
BufferedReader reader = new BufferedReader(new FileReader(getFile()));
String line = "";
boolean confLocked = false;
Map<String,String> result = new HashMap<String, String>();
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);
}
}
private Collection<ParsedRule<?>> 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; // substring match, case insensitive
for (String c : rule.categories) if (c.equals(search)) return true; // category exactly, case sensitive
if (Sets.newHashSet(rule.description.toLowerCase(Locale.ROOT).split("\\W+")).contains(lcSearch))
return true; // contains full term in description, but case insensitive
return false;
}).sorted().collect(ImmutableList.toImmutableList());
}
public int printAllRulesToLog(String category)
{
PrintStream ps = System.out;
ps.println("# "+fancyName+" Settings");
for (Map.Entry<String, ParsedRule<?>> e : new TreeMap<>(rules).entrySet())
{
ParsedRule<?> rule = e.getValue();
if (category != null && !rule.categories.contains(category))
continue;
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<ServerCommandSource> 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<ServerCommandSource> 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<ServerCommandSource> 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.greedyString()).
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.greedyString()).
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<BaseText> 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<BaseText> 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<Map<String, String>,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<Map<String, String>,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<Object> 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<ParsedRule<?>> 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, "g "+fancyName+" version: "+ version);
try
{
PlayerEntity player = source.getPlayer();
List<Object> 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;
}
}