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