001/*
002 * This file is part of Baritone.
003 *
004 * Baritone is free software: you can redistribute it and/or modify
005 * it under the terms of the GNU Lesser General Public License as published by
006 * the Free Software Foundation, either version 3 of the License, or
007 * (at your option) any later version.
008 *
009 * Baritone is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
012 * GNU Lesser General Public License for more details.
013 *
014 * You should have received a copy of the GNU Lesser General Public License
015 * along with Baritone.  If not, see <https://www.gnu.org/licenses/>.
016 */
017
018package baritone.api.utils;
019
020import baritone.api.BaritoneAPI;
021import baritone.api.Settings;
022import net.minecraft.block.Block;
023import net.minecraft.item.Item;
024import net.minecraft.util.EnumFacing;
025import net.minecraft.util.math.Vec3i;
026
027import java.awt.*;
028import java.io.BufferedReader;
029import java.io.BufferedWriter;
030import java.io.IOException;
031import java.lang.reflect.ParameterizedType;
032import java.lang.reflect.Type;
033import java.nio.file.Files;
034import java.nio.file.NoSuchFileException;
035import java.nio.file.Path;
036import java.util.ArrayList;
037import java.util.List;
038import java.util.Map;
039import java.util.Objects;
040import java.util.function.Consumer;
041import java.util.function.Function;
042import java.util.regex.Matcher;
043import java.util.regex.Pattern;
044import java.util.stream.Collectors;
045import java.util.stream.Stream;
046
047import static net.minecraft.client.Minecraft.getMinecraft;
048
049public class SettingsUtil {
050
051    private static final Path SETTINGS_PATH = getMinecraft().gameDir.toPath().resolve("baritone").resolve("settings.txt");
052    private static final Pattern SETTING_PATTERN = Pattern.compile("^(?<setting>[^ ]+) +(?<value>.+)"); // key and value split by the first space
053    private static final String[] JAVA_ONLY_SETTINGS = {"logger", "notifier", "toaster"};
054
055    private static boolean isComment(String line) {
056        return line.startsWith("#") || line.startsWith("//");
057    }
058
059    private static void forEachLine(Path file, Consumer<String> consumer) throws IOException {
060        try (BufferedReader scan = Files.newBufferedReader(file)) {
061            String line;
062            while ((line = scan.readLine()) != null) {
063                if (line.isEmpty() || isComment(line)) {
064                    continue;
065                }
066                consumer.accept(line);
067            }
068        }
069    }
070
071    public static void readAndApply(Settings settings) {
072        try {
073            forEachLine(SETTINGS_PATH, line -> {
074                Matcher matcher = SETTING_PATTERN.matcher(line);
075                if (!matcher.matches()) {
076                    System.out.println("Invalid syntax in setting file: " + line);
077                    return;
078                }
079
080                String settingName = matcher.group("setting").toLowerCase();
081                String settingValue = matcher.group("value");
082                try {
083                    parseAndApply(settings, settingName, settingValue);
084                } catch (Exception ex) {
085                    System.out.println("Unable to parse line " + line);
086                    ex.printStackTrace();
087                }
088            });
089        } catch (NoSuchFileException ignored) {
090            System.out.println("Baritone settings file not found, resetting.");
091        } catch (Exception ex) {
092            System.out.println("Exception while reading Baritone settings, some settings may be reset to default values!");
093            ex.printStackTrace();
094        }
095    }
096
097    public static synchronized void save(Settings settings) {
098        try (BufferedWriter out = Files.newBufferedWriter(SETTINGS_PATH)) {
099            for (Settings.Setting setting : modifiedSettings(settings)) {
100                out.write(settingToString(setting) + "\n");
101            }
102        } catch (Exception ex) {
103            System.out.println("Exception thrown while saving Baritone settings!");
104            ex.printStackTrace();
105        }
106    }
107
108    public static List<Settings.Setting> modifiedSettings(Settings settings) {
109        List<Settings.Setting> modified = new ArrayList<>();
110        for (Settings.Setting setting : settings.allSettings) {
111            if (setting.value == null) {
112                System.out.println("NULL SETTING?" + setting.getName());
113                continue;
114            }
115            if (javaOnlySetting(setting)) {
116                continue; // NO
117            }
118            if (setting.value == setting.defaultValue) {
119                continue;
120            }
121            modified.add(setting);
122        }
123        return modified;
124    }
125
126    /**
127     * Gets the type of a setting and returns it as a string, with package names stripped.
128     * <p>
129     * For example, if the setting type is {@code java.util.List<java.lang.String>}, this function returns
130     * {@code List<String>}.
131     *
132     * @param setting The setting
133     * @return The type
134     */
135    public static String settingTypeToString(Settings.Setting setting) {
136        return setting.getType().getTypeName()
137                .replaceAll("(?:\\w+\\.)+(\\w+)", "$1");
138    }
139
140    public static <T> String settingValueToString(Settings.Setting<T> setting, T value) throws IllegalArgumentException {
141        Parser io = Parser.getParser(setting.getType());
142
143        if (io == null) {
144            throw new IllegalStateException("Missing " + setting.getValueClass() + " " + setting.getName());
145        }
146
147        return io.toString(new ParserContext(setting), value);
148    }
149
150    public static String settingValueToString(Settings.Setting setting) throws IllegalArgumentException {
151        //noinspection unchecked
152        return settingValueToString(setting, setting.value);
153    }
154
155    public static String settingDefaultToString(Settings.Setting setting) throws IllegalArgumentException {
156        //noinspection unchecked
157        return settingValueToString(setting, setting.defaultValue);
158    }
159
160    public static String maybeCensor(int coord) {
161        if (BaritoneAPI.getSettings().censorCoordinates.value) {
162            return "<censored>";
163        }
164
165        return Integer.toString(coord);
166    }
167
168    public static String settingToString(Settings.Setting setting) throws IllegalStateException {
169        if (javaOnlySetting(setting)) {
170            return setting.getName();
171        }
172
173        return setting.getName() + " " + settingValueToString(setting);
174    }
175
176    /**
177     * This should always be the same as whether the setting can be parsed from or serialized to a string
178     *
179     * @param the setting
180     * @return true if the setting can not be set or read by the user
181     */
182    public static boolean javaOnlySetting(Settings.Setting setting) {
183        for (String name : JAVA_ONLY_SETTINGS) { // no JAVA_ONLY_SETTINGS.contains(...) because that would be case sensitive
184            if (setting.getName().equalsIgnoreCase(name)) {
185                return true;
186            }
187        }
188        return false;
189    }
190
191    public static void parseAndApply(Settings settings, String settingName, String settingValue) throws IllegalStateException, NumberFormatException {
192        Settings.Setting setting = settings.byLowerName.get(settingName);
193        if (setting == null) {
194            throw new IllegalStateException("No setting by that name");
195        }
196        Class intendedType = setting.getValueClass();
197        ISettingParser ioMethod = Parser.getParser(setting.getType());
198        Object parsed = ioMethod.parse(new ParserContext(setting), settingValue);
199        if (!intendedType.isInstance(parsed)) {
200            throw new IllegalStateException(ioMethod + " parser returned incorrect type, expected " + intendedType + " got " + parsed + " which is " + parsed.getClass());
201        }
202        setting.value = parsed;
203    }
204
205    private interface ISettingParser<T> {
206
207        T parse(ParserContext context, String raw);
208
209        String toString(ParserContext context, T value);
210
211        boolean accepts(Type type);
212    }
213
214    private static class ParserContext {
215
216        private final Settings.Setting<?> setting;
217
218        private ParserContext(Settings.Setting<?> setting) {
219            this.setting = setting;
220        }
221
222        private Settings.Setting<?> getSetting() {
223            return this.setting;
224        }
225    }
226
227    private enum Parser implements ISettingParser {
228
229        DOUBLE(Double.class, Double::parseDouble),
230        BOOLEAN(Boolean.class, Boolean::parseBoolean),
231        INTEGER(Integer.class, Integer::parseInt),
232        FLOAT(Float.class, Float::parseFloat),
233        LONG(Long.class, Long::parseLong),
234        STRING(String.class, String::new),
235        ENUMFACING(EnumFacing.class, EnumFacing::byName),
236        COLOR(
237                Color.class,
238                str -> new Color(Integer.parseInt(str.split(",")[0]), Integer.parseInt(str.split(",")[1]), Integer.parseInt(str.split(",")[2])),
239                color -> color.getRed() + "," + color.getGreen() + "," + color.getBlue()
240        ),
241        VEC3I(
242                Vec3i.class,
243                str -> new Vec3i(Integer.parseInt(str.split(",")[0]), Integer.parseInt(str.split(",")[1]), Integer.parseInt(str.split(",")[2])),
244                vec -> vec.getX() + "," + vec.getY() + "," + vec.getZ()
245        ),
246        BLOCK(
247                Block.class,
248                str -> BlockUtils.stringToBlockRequired(str.trim()),
249                BlockUtils::blockToString
250        ),
251        ITEM(
252                Item.class,
253                str -> Item.getByNameOrId(str.trim()),
254                item -> Item.REGISTRY.getNameForObject(item).toString()
255        ),
256        LIST() {
257            @Override
258            public Object parse(ParserContext context, String raw) {
259                Type type = ((ParameterizedType) context.getSetting().getType()).getActualTypeArguments()[0];
260                Parser parser = Parser.getParser(type);
261
262                return Stream.of(raw.split(","))
263                        .map(s -> parser.parse(context, s))
264                        .collect(Collectors.toList());
265            }
266
267            @Override
268            public String toString(ParserContext context, Object value) {
269                Type type = ((ParameterizedType) context.getSetting().getType()).getActualTypeArguments()[0];
270                Parser parser = Parser.getParser(type);
271
272                return ((List<?>) value).stream()
273                        .map(o -> parser.toString(context, o))
274                        .collect(Collectors.joining(","));
275            }
276
277            @Override
278            public boolean accepts(Type type) {
279                return List.class.isAssignableFrom(TypeUtils.resolveBaseClass(type));
280            }
281        },
282        MAPPING() {
283            @Override
284            public Object parse(ParserContext context, String raw) {
285                Type keyType = ((ParameterizedType) context.getSetting().getType()).getActualTypeArguments()[0];
286                Type valueType = ((ParameterizedType) context.getSetting().getType()).getActualTypeArguments()[1];
287                Parser keyParser = Parser.getParser(keyType);
288                Parser valueParser = Parser.getParser(valueType);
289
290                return Stream.of(raw.split(",(?=[^,]*->)"))
291                        .map(s -> s.split("->"))
292                        .collect(Collectors.toMap(s -> keyParser.parse(context, s[0]), s -> valueParser.parse(context, s[1])));
293            }
294
295            @Override
296            public String toString(ParserContext context, Object value) {
297                Type keyType = ((ParameterizedType) context.getSetting().getType()).getActualTypeArguments()[0];
298                Type valueType = ((ParameterizedType) context.getSetting().getType()).getActualTypeArguments()[1];
299                Parser keyParser = Parser.getParser(keyType);
300                Parser valueParser = Parser.getParser(valueType);
301
302                return ((Map<?,?>) value).entrySet().stream()
303                        .map(o -> keyParser.toString(context, o.getKey()) + "->" + valueParser.toString(context, o.getValue()))
304                        .collect(Collectors.joining(","));
305            }
306
307            @Override
308            public boolean accepts(Type type) {
309                return Map.class.isAssignableFrom(TypeUtils.resolveBaseClass(type));
310            }
311        };
312
313        private final Class<?> cla$$;
314        private final Function<String, Object> parser;
315        private final Function<Object, String> toString;
316
317        Parser() {
318            this.cla$$ = null;
319            this.parser = null;
320            this.toString = null;
321        }
322
323        <T> Parser(Class<T> cla$$, Function<String, T> parser) {
324            this(cla$$, parser, Object::toString);
325        }
326
327        <T> Parser(Class<T> cla$$, Function<String, T> parser, Function<T, String> toString) {
328            this.cla$$ = cla$$;
329            this.parser = parser::apply;
330            this.toString = x -> toString.apply((T) x);
331        }
332
333        @Override
334        public Object parse(ParserContext context, String raw) {
335            Object parsed = this.parser.apply(raw);
336            Objects.requireNonNull(parsed);
337            return parsed;
338        }
339
340        @Override
341        public String toString(ParserContext context, Object value) {
342            return this.toString.apply(value);
343        }
344
345        @Override
346        public boolean accepts(Type type) {
347            return type instanceof Class && this.cla$$.isAssignableFrom((Class) type);
348        }
349
350        public static Parser getParser(Type type) {
351            return Stream.of(values())
352                    .filter(parser -> parser.accepts(type))
353                    .findFirst().orElse(null);
354        }
355    }
356}