Path: blob/1.21.x/fmlcore/src/main/java/net/minecraftforge/fml/config/ConfigFileTypeHandler.java
7350 views
/*1* Copyright (c) Forge Development LLC and contributors2* SPDX-License-Identifier: LGPL-2.1-only3*/45package net.minecraftforge.fml.config;67import com.electronwill.nightconfig.core.ConfigFormat;8import com.electronwill.nightconfig.core.file.CommentedFileConfig;9import com.electronwill.nightconfig.core.file.FileWatcher;10import com.electronwill.nightconfig.core.io.ParsingException;11import com.electronwill.nightconfig.core.io.WritingMode;12import com.mojang.logging.LogUtils;13import net.minecraftforge.fml.loading.FMLConfig;14import net.minecraftforge.fml.loading.FMLPaths;15import org.apache.commons.io.FilenameUtils;16import org.jetbrains.annotations.Nullable;17import org.slf4j.Logger;1819import java.io.IOException;20import java.nio.file.Files;21import java.nio.file.Path;22import java.util.function.Function;2324import static net.minecraftforge.fml.config.ConfigTracker.CONFIG;2526public class ConfigFileTypeHandler {27private static final Logger LOGGER = LogUtils.getLogger();28private static final Path defaultConfigPath = FMLPaths.GAMEDIR.get().resolve(FMLConfig.getConfigValue(FMLConfig.ConfigValue.DEFAULT_CONFIG_PATH));2930private static final ConfigFileTypeHandler CLIENT = new ConfigFileTypeHandler(ModConfig.Type.CLIENT);31private static final ConfigFileTypeHandler COMMON = new ConfigFileTypeHandler(ModConfig.Type.COMMON);32private static final ConfigFileTypeHandler SERVER = new ConfigFileTypeHandler(ModConfig.Type.SERVER);3334private final @Nullable ModConfig.Type type;35private @Nullable FileWatcher watcher;3637// exists for bin compat38public ConfigFileTypeHandler() {39this(null);40}4142private ConfigFileTypeHandler(@Nullable ModConfig.Type type) {43this.type = type;44}4546/**47* Gets the handler for the given {@link ModConfig.Type}.48*49* @param type The type to get the handler for50* @return The handler51*/52static ConfigFileTypeHandler get(ModConfig.Type type) {53return switch (type) {54case CLIENT -> CLIENT;55case COMMON -> COMMON;56case SERVER -> SERVER;57};58}5960/**61* Gets the {@link FileWatcher} for this handler, creating it if it doesn't exist and/or has been stopped.62*63* @return The watcher64*65* @apiNote This is package-private so modders can't just call {@link FileWatcher#stop()} and fuck up everything.66*/67FileWatcher getWatcher() {68if (this.watcher == null) {69LOGGER.debug(CONFIG, "Starting watcher for handler: {}", this);70this.watcher = new FileWatcher();71}7273return this.watcher;74}7576/**77* Stops the {@link FileWatcher} for this handler, and sets it to null afterward. Use this instead of78* {@link FileWatcher#stop()}.79*/80void stopWatcher() {81if (this.watcher == null) return;8283LOGGER.debug(CONFIG, "Stopping watcher for hander: {}", this);84this.watcher.stop();85this.watcher = null;86}8788public Function<ModConfig, CommentedFileConfig> reader(Path configBasePath) {89return (c) -> {90final Path configPath = configBasePath.resolve(c.getFileName());91final CommentedFileConfig configData = CommentedFileConfig.builder(configPath).sync().92preserveInsertionOrder().93autosave().94onFileNotFound((newfile, configFormat)-> setupConfigFile(c, newfile, configFormat)).95writingMode(WritingMode.REPLACE).96build();97LOGGER.debug(CONFIG, "Built TOML config for {}", configPath);98try99{100configData.load();101}102catch (ParsingException ex)103{104throw new ConfigLoadingException(c, ex);105}106LOGGER.debug(CONFIG, "Loaded TOML config file {}", configPath);107this.getWatcher().addWatch(configPath, new ConfigWatcher(c, configData, Thread.currentThread().getContextClassLoader()));108LOGGER.debug(CONFIG, "Watching TOML config file {} for changes", configPath);109return configData;110};111}112113public void unload(Path configBasePath, ModConfig config) {114Path configPath = configBasePath.resolve(config.getFileName());115try {116this.getWatcher().removeWatch(configPath);117} catch (RuntimeException e) {118LOGGER.error("Failed to remove config {} from tracker!", configPath, e);119}120}121122private static boolean setupConfigFile(final ModConfig modConfig, final Path file, final ConfigFormat<?> conf) throws IOException {123if (!Files.isDirectory(file.getParent())) {124Files.createDirectories(file.getParent());125}126Path p = defaultConfigPath.resolve(modConfig.getFileName());127if (Files.exists(p)) {128LOGGER.info(CONFIG, "Loading default config file from path {}", p);129Files.copy(p, file);130} else {131Files.createFile(file);132conf.initEmptyFile(file);133}134return true;135}136137public static void backUpConfig(final CommentedFileConfig commentedFileConfig)138{139backUpConfig(commentedFileConfig, 5); //TODO: Think of a way for mods to set their own preference (include a sanity check as well, no disk stuffing)140}141142public static void backUpConfig(final CommentedFileConfig commentedFileConfig, final int maxBackups)143{144Path bakFileLocation = commentedFileConfig.getNioPath().getParent();145String bakFileName = FilenameUtils.removeExtension(commentedFileConfig.getFile().getName());146String bakFileExtension = FilenameUtils.getExtension(commentedFileConfig.getFile().getName()) + ".bak";147Path bakFile = bakFileLocation.resolve(bakFileName + "-1" + "." + bakFileExtension);148try149{150for(int i = maxBackups; i > 0; i--)151{152Path oldBak = bakFileLocation.resolve(bakFileName + "-" + i + "." + bakFileExtension);153if(Files.exists(oldBak))154{155if(i >= maxBackups)156Files.delete(oldBak);157else158Files.move(oldBak, bakFileLocation.resolve(bakFileName + "-" + (i + 1) + "." + bakFileExtension));159}160}161Files.copy(commentedFileConfig.getNioPath(), bakFile);162}163catch (IOException exception)164{165LOGGER.warn(CONFIG, "Failed to back up config file {}", commentedFileConfig.getNioPath(), exception);166}167}168169@Override170public String toString() {171return "ConfigFileTypeHandler[" + (type != null ? type : "UNKNOWN") + "]";172}173174private record ConfigWatcher(175ModConfig modConfig,176CommentedFileConfig commentedFileConfig,177ClassLoader realClassLoader178) implements Runnable {179@Override180public void run() {181// Force the regular classloader onto the special thread182Thread.currentThread().setContextClassLoader(realClassLoader);183if (!this.modConfig.getSpec().isCorrecting()) {184try185{186this.commentedFileConfig.load();187if(!this.modConfig.getSpec().isCorrect(commentedFileConfig))188{189LOGGER.warn(CONFIG, "Configuration file {} is not correct. Correcting", commentedFileConfig.getFile().getAbsolutePath());190ConfigFileTypeHandler.backUpConfig(commentedFileConfig);191this.modConfig.getSpec().correct(commentedFileConfig);192commentedFileConfig.save();193}194}195catch (ParsingException ex)196{197throw new ConfigLoadingException(modConfig, ex);198}199LOGGER.debug(CONFIG, "Config file {} changed, sending notifies", this.modConfig.getFileName());200this.modConfig.getSpec().afterReload();201this.modConfig.fireEvent(IConfigEvent.reloading(this.modConfig));202}203}204}205206private static class ConfigLoadingException extends RuntimeException207{208public ConfigLoadingException(ModConfig config, Exception cause)209{210super("Failed loading config file " + config.getFileName() + " of type " + config.getType() + " for modid " + config.getModId(), cause);211}212}213}214215216