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.Objects;
039import java.util.function.Consumer;
040import java.util.function.Function;
041import java.util.regex.Matcher;
042import java.util.regex.Pattern;
043import java.util.stream.Collectors;
044import java.util.stream.Stream;
045
046import static net.minecraft.client.Minecraft.getMinecraft;
047
048public class SettingsUtil {
049
050    private static final Path SETTINGS_PATH = getMinecraft().gameDir.toPath().resolve("baritone").resolve("settings.txt");
051    private static final Pattern SETTING_PATTERN = Pattern.compile("^(?<setting>[^ ]+) +(?<value>.+)"); // key and value split by the first space
052
053    private static boolean isComment(String line) {
054        return line.startsWith("#") || line.startsWith("//");
055    }
056
057    private static void forEachLine(Path file, Consumer<String> consumer) throws IOException {
058        try (BufferedReader scan = Files.newBufferedReader(file)) {
059            String line;
060            while ((line = scan.readLine()) != null) {
061                if (line.isEmpty() || isComment(line)) {
062                    continue;
063                }
064                consumer.accept(line);
065            }
066        }
067    }
068
069    public static void readAndApply(Settings settings) {
070        try {
071            forEachLine(SETTINGS_PATH, line -> {
072                Matcher matcher = SETTING_PATTERN.matcher(line);
073                if (!matcher.matches()) {
074                    System.out.println("Invalid syntax in setting file: " + line);
075                    return;
076                }
077
078                String settingName = matcher.group("setting").toLowerCase();
079                String settingValue = matcher.group("value");
080                try {
081                    parseAndApply(settings, settingName, settingValue);
082                } catch (Exception ex) {
083                    System.out.println("Unable to parse line " + line);
084                    ex.printStackTrace();
085                }
086            });
087        } catch (NoSuchFileException ignored) {
088            System.out.println("Baritone settings file not found, resetting.");
089        } catch (Exception ex) {
090            System.out.println("Exception while reading Baritone settings, some settings may be reset to default values!");
091            ex.printStackTrace();
092        }
093    }
094
095    public static synchronized void save(Settings settings) {
096        try (BufferedWriter out = Files.newBufferedWriter(SETTINGS_PATH)) {
097            for (Settings.Setting setting : modifiedSettings(settings)) {
098                out.write(settingToString(setting) + "\n");
099            }
100        } catch (Exception ex) {
101            System.out.println("Exception thrown while saving Baritone settings!");
102            ex.printStackTrace();
103        }
104    }
105
106    public static List<Settings.Setting> modifiedSettings(Settings settings) {
107        List<Settings.Setting> modified = new ArrayList<>();
108        for (Settings.Setting setting : settings.allSettings) {
109            if (setting.value == null) {
110                System.out.println("NULL SETTING?" + setting.getName());
111                continue;
112            }
113            if (setting.getName().equals("logger")) {
114                continue; // NO
115            }
116            if (setting.value == setting.defaultValue) {
117                continue;
118            }
119            modified.add(setting);
120        }
121        return modified;
122    }
123
124    /**
125     * Gets the type of a setting and returns it as a string, with package names stripped.
126     * <p>
127     * For example, if the setting type is {@code java.util.List<java.lang.String>}, this function returns
128     * {@code List<String>}.
129     *
130     * @param setting The setting
131     * @return The type
132     */
133    public static String settingTypeToString(Settings.Setting setting) {
134        return setting.getType().getTypeName()
135                .replaceAll("(?:\\w+\\.)+(\\w+)", "$1");
136    }
137
138    public static <T> String settingValueToString(Settings.Setting<T> setting, T value) throws IllegalArgumentException {
139        Parser io = Parser.getParser(setting.getType());
140
141        if (io == null) {
142            throw new IllegalStateException("Missing " + setting.getValueClass() + " " + setting.getName());
143        }
144
145        return io.toString(new ParserContext(setting), value);
146    }
147
148    public static String settingValueToString(Settings.Setting setting) throws IllegalArgumentException {
149        //noinspection unchecked
150        return settingValueToString(setting, setting.value);
151    }
152
153    public static String settingDefaultToString(Settings.Setting setting) throws IllegalArgumentException {
154        //noinspection unchecked
155        return settingValueToString(setting, setting.defaultValue);
156    }
157
158    public static String maybeCensor(int coord) {
159        if (BaritoneAPI.getSettings().censorCoordinates.value) {
160            return "<censored>";
161        }
162
163        return Integer.toString(coord);
164    }
165
166    public static String settingToString(Settings.Setting setting) throws IllegalStateException {
167        if (setting.getName().equals("logger")) {
168            return "logger";
169        }
170
171        return setting.getName() + " " + settingValueToString(setting);
172    }
173
174    public static void parseAndApply(Settings settings, String settingName, String settingValue) throws IllegalStateException, NumberFormatException {
175        Settings.Setting setting = settings.byLowerName.get(settingName);
176        if (setting == null) {
177            throw new IllegalStateException("No setting by that name");
178        }
179        Class intendedType = setting.getValueClass();
180        ISettingParser ioMethod = Parser.getParser(setting.getType());
181        Object parsed = ioMethod.parse(new ParserContext(setting), settingValue);
182        if (!intendedType.isInstance(parsed)) {
183            throw new IllegalStateException(ioMethod + " parser returned incorrect type, expected " + intendedType + " got " + parsed + " which is " + parsed.getClass());
184        }
185        setting.value = parsed;
186    }
187
188    private interface ISettingParser<T> {
189
190        T parse(ParserContext context, String raw);
191
192        String toString(ParserContext context, T value);
193
194        boolean accepts(Type type);
195    }
196
197    private static class ParserContext {
198
199        private final Settings.Setting<?> setting;
200
201        private ParserContext(Settings.Setting<?> setting) {
202            this.setting = setting;
203        }
204
205        private Settings.Setting<?> getSetting() {
206            return this.setting;
207        }
208    }
209
210    private enum Parser implements ISettingParser {
211
212        DOUBLE(Double.class, Double::parseDouble),
213        BOOLEAN(Boolean.class, Boolean::parseBoolean),
214        INTEGER(Integer.class, Integer::parseInt),
215        FLOAT(Float.class, Float::parseFloat),
216        LONG(Long.class, Long::parseLong),
217        STRING(String.class, String::new),
218        ENUMFACING(EnumFacing.class, EnumFacing::byName),
219        COLOR(
220                Color.class,
221                str -> new Color(Integer.parseInt(str.split(",")[0]), Integer.parseInt(str.split(",")[1]), Integer.parseInt(str.split(",")[2])),
222                color -> color.getRed() + "," + color.getGreen() + "," + color.getBlue()
223        ),
224        VEC3I(
225                Vec3i.class,
226                str -> new Vec3i(Integer.parseInt(str.split(",")[0]), Integer.parseInt(str.split(",")[1]), Integer.parseInt(str.split(",")[2])),
227                vec -> vec.getX() + "," + vec.getY() + "," + vec.getZ()
228        ),
229        BLOCK(
230                Block.class,
231                str -> BlockUtils.stringToBlockRequired(str.trim()),
232                BlockUtils::blockToString
233        ),
234        ITEM(
235                Item.class,
236                str -> Item.getByNameOrId(str.trim()),
237                item -> Item.REGISTRY.getNameForObject(item).toString()
238        ),
239        LIST() {
240            @Override
241            public Object parse(ParserContext context, String raw) {
242                Type type = ((ParameterizedType) context.getSetting().getType()).getActualTypeArguments()[0];
243                Parser parser = Parser.getParser(type);
244
245                return Stream.of(raw.split(","))
246                        .map(s -> parser.parse(context, s))
247                        .collect(Collectors.toList());
248            }
249
250            @Override
251            public String toString(ParserContext context, Object value) {
252                Type type = ((ParameterizedType) context.getSetting().getType()).getActualTypeArguments()[0];
253                Parser parser = Parser.getParser(type);
254
255                return ((List<?>) value).stream()
256                        .map(o -> parser.toString(context, o))
257                        .collect(Collectors.joining(","));
258            }
259
260            @Override
261            public boolean accepts(Type type) {
262                return List.class.isAssignableFrom(TypeUtils.resolveBaseClass(type));
263            }
264        };
265
266        private final Class<?> cla$$;
267        private final Function<String, Object> parser;
268        private final Function<Object, String> toString;
269
270        Parser() {
271            this.cla$$ = null;
272            this.parser = null;
273            this.toString = null;
274        }
275
276        <T> Parser(Class<T> cla$$, Function<String, T> parser) {
277            this(cla$$, parser, Object::toString);
278        }
279
280        <T> Parser(Class<T> cla$$, Function<String, T> parser, Function<T, String> toString) {
281            this.cla$$ = cla$$;
282            this.parser = parser::apply;
283            this.toString = x -> toString.apply((T) x);
284        }
285
286        @Override
287        public Object parse(ParserContext context, String raw) {
288            Object parsed = this.parser.apply(raw);
289            Objects.requireNonNull(parsed);
290            return parsed;
291        }
292
293        @Override
294        public String toString(ParserContext context, Object value) {
295            return this.toString.apply(value);
296        }
297
298        @Override
299        public boolean accepts(Type type) {
300            return type instanceof Class && this.cla$$.isAssignableFrom((Class) type);
301        }
302
303        public static Parser getParser(Type type) {
304            return Stream.of(values())
305                    .filter(parser -> parser.accepts(type))
306                    .findFirst().orElse(null);
307        }
308    }
309}