001/* 002 * Copyright 2020 zml 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package ca.stellardrift.confabricate; 017 018import ca.stellardrift.confabricate.typeserializers.MinecraftSerializers; 019import com.google.errorprone.annotations.RestrictedApi; 020import com.mojang.datafixers.DSL; 021import com.mojang.datafixers.DataFixer; 022import com.mojang.serialization.Dynamic; 023import java.io.IOException; 024import java.nio.file.Files; 025import java.nio.file.Path; 026import net.fabricmc.api.ModInitializer; 027import net.fabricmc.loader.api.FabricLoader; 028import net.fabricmc.loader.api.ModContainer; 029import net.minecraft.SharedConstants; 030import net.minecraft.resources.ResourceLocation; 031import net.minecraft.util.datafix.DataFixers; 032import org.apache.logging.log4j.LogManager; 033import org.apache.logging.log4j.Logger; 034import org.spongepowered.configurate.CommentedConfigurationNode; 035import org.spongepowered.configurate.ConfigurateException; 036import org.spongepowered.configurate.ConfigurationNode; 037import org.spongepowered.configurate.ConfigurationOptions; 038import org.spongepowered.configurate.NodePath; 039import org.spongepowered.configurate.extra.dfu.v4.ConfigurateOps; 040import org.spongepowered.configurate.extra.dfu.v4.DataFixerTransformation; 041import org.spongepowered.configurate.hocon.HoconConfigurationLoader; 042import org.spongepowered.configurate.loader.ConfigurationLoader; 043import org.spongepowered.configurate.reference.ConfigurationReference; 044import org.spongepowered.configurate.reference.WatchServiceListener; 045import org.spongepowered.configurate.transformation.ConfigurationTransformation; 046import org.spongepowered.configurate.transformation.TransformAction; 047 048/** 049 * Configurate integration holder, providing access to configuration loaders 050 * pre-configured to work with Minecraft types. 051 * 052 * <p>This class has static utility methods for usage by other mods -- it should 053 * not be instantiated by anyone but the mod loader. 054 * 055 * @since 1.0.0 056 */ 057public class Confabricate implements ModInitializer { 058 059 static final String MOD_ID = "confabricate"; 060 061 static final Logger LOGGER = LogManager.getLogger(); 062 063 private static WatchServiceListener listener; 064 065 static { 066 try { 067 Confabricate.listener = WatchServiceListener.create(); 068 Runtime.getRuntime().addShutdownHook(new Thread(() -> { 069 try { 070 Confabricate.listener.close(); 071 } catch (final IOException e) { 072 LOGGER.catching(e); 073 } 074 }, "Confabricate shutdown thread")); 075 } catch (final IOException e) { 076 LOGGER.error("Could not initialize file listener", e); 077 } 078 } 079 080 /** 081 * Internal API to get a mod {@link ResourceLocation}. 082 * 083 * @param item path value 084 * @return new identifier 085 * @since 2.0.0 086 */ 087 @RestrictedApi(explanation = "confabricate namespace is not open to others", 088 link = "", allowedOnPath = ".*/ca/stellardrift/confabricate/.*") 089 public static ResourceLocation id(final String item) { 090 return new ResourceLocation(MOD_ID, item); 091 } 092 093 @Override 094 public void onInitialize() { 095 // initialize serializers early, fail fast 096 MinecraftSerializers.collection(); 097 } 098 099 /** 100 * Get configuration options configured to use Confabricate's serializers. 101 * 102 * @return customized options 103 * @since 2.0.0 104 */ 105 public static ConfigurationOptions confabricateOptions() { 106 return ConfigurationOptions.defaults() 107 .serializers(MinecraftSerializers.collection()); 108 } 109 110 /** 111 * Create a configuration loader for the given mod's main 112 * configuration file. 113 * 114 * <p>By default, this config file is in a dedicated directory for the mod. 115 * 116 * @param mod the mod wanting to access its config 117 * @return a configuration loader in the Hocon format 118 * @see #loaderFor(ModContainer, boolean, ConfigurationOptions) 119 * @since 1.0.0 120 */ 121 public static ConfigurationLoader<CommentedConfigurationNode> loaderFor(final ModContainer mod) { 122 return loaderFor(mod, true); 123 } 124 125 /** 126 * Get a configuration loader for a mod. The configuration will be in 127 * Hocon format. 128 * 129 * <p>If the configuration is in its own directory, the path will be 130 * <pre><config root>/<modid>/<modid>.conf</pre>. 131 * Otherwise, the path will be 132 * <pre><config root>/<modid>.conf</pre>. 133 * 134 * <p>The returned {@link ConfigurationLoader ConfigurationLoaders} will be 135 * pre-configured to use the type serializers from 136 * {@link MinecraftSerializers#collection()}, but will otherwise use 137 * default settings. 138 * 139 * @param mod the mod to get the configuration loader for 140 * @param ownDirectory whether the configuration should be in a directory 141 * just for the mod, or a file in the config root 142 * @return the newly created configuration loader 143 * @since 1.0.0 144 */ 145 public static ConfigurationLoader<CommentedConfigurationNode> loaderFor(final ModContainer mod, final boolean ownDirectory) { 146 return loaderFor(mod, ownDirectory, confabricateOptions()); 147 } 148 149 /** 150 * Get a configuration loader for a mod. The configuration will be in 151 * Hocon format. 152 * 153 * <p>If the configuration is in its own directory, the path will be 154 * <pre><config root>/<modid>/<modid>.conf</pre>. 155 * Otherwise, the path will be 156 * <pre><config root>/<modid>.conf</pre>. 157 * 158 * <p>The returned {@link ConfigurationLoader ConfigurationLoaders} will be 159 * pre-configured to use the type serializers from 160 * {@link MinecraftSerializers#collection()}, but will otherwise use 161 * default settings. 162 * 163 * @param mod the mod to get the configuration loader for 164 * @param ownDirectory whether the configuration should be in a directory 165 * just for the mod, or a file in the config root 166 * @param options the options to use by default when loading 167 * @return the newly created configuration loader 168 * @since 2.0.0 169 */ 170 public static ConfigurationLoader<CommentedConfigurationNode> loaderFor( 171 final ModContainer mod, 172 final boolean ownDirectory, 173 final ConfigurationOptions options) { 174 return HoconConfigurationLoader.builder() 175 .path(configurationFile(mod, ownDirectory)) 176 .defaultOptions(options) 177 .build(); 178 179 } 180 181 /** 182 * Create a configuration reference to the provided mod's main 183 * configuration file. 184 * 185 * <p>By default, this config file is in a dedicated directory for the mod. 186 * The returned reference will automatically reload. 187 * 188 * @param mod the mod wanting to access its config 189 * @return a configuration reference for a loaded node in HOCON format 190 * @throws ConfigurateException if a listener could not be established or if 191 * the configuration failed to load. 192 * @see #configurationFor(ModContainer, boolean, ConfigurationOptions) 193 * @since 1.1.0 194 */ 195 public static ConfigurationReference<CommentedConfigurationNode> configurationFor(final ModContainer mod) throws ConfigurateException { 196 return configurationFor(mod, true); 197 } 198 199 /** 200 * Get a configuration reference for a mod. The configuration will be in 201 * Hocon format. 202 * 203 * <p>If the configuration is in its own directory, the path will be 204 * <pre><config root>/<modid>/<modid>.conf</pre> 205 * Otherwise, the path will be 206 * <pre><config root>/<modid>.conf</pre>. 207 * 208 * <p>The reference's {@link ConfigurationLoader} will be pre-configured to 209 * use the type serializers from {@link MinecraftSerializers#collection()} 210 * but will otherwise use default settings. 211 * 212 * @param mod the mod to get the configuration loader for 213 * @param ownDirectory whether the configuration should be in a directory 214 * just for the mod 215 * @return the newly created and loaded configuration reference 216 * @throws ConfigurateException if a listener could not be established or 217 * the configuration failed to load. 218 * @since 1.1.0 219 */ 220 public static ConfigurationReference<CommentedConfigurationNode> configurationFor( 221 final ModContainer mod, 222 final boolean ownDirectory) throws ConfigurateException { 223 return configurationFor(mod, ownDirectory, confabricateOptions()); 224 } 225 226 /** 227 * Get a configuration reference for a mod. The configuration will be in 228 * Hocon format. 229 * 230 * <p>If the configuration is in its own directory, the path will be 231 * <pre><config root>/<modid>/<modid>.conf</pre> 232 * Otherwise, the path will be 233 * <pre><config root>/<modid>.conf</pre>. 234 * 235 * <p>The reference's {@link ConfigurationLoader} will be pre-configured to 236 * use the type serializers from {@link MinecraftSerializers#collection()} 237 * but will otherwise use default settings. 238 * 239 * @param mod the mod to get the configuration loader for 240 * @param ownDirectory whether the configuration should be in a directory 241 * just for the mod 242 * @param options the options to use by default when loading 243 * @return the newly created and loaded configuration reference 244 * @throws ConfigurateException if a listener could not be established or 245 * the configuration failed to load. 246 * @since 2.0.0 247 */ 248 public static ConfigurationReference<CommentedConfigurationNode> configurationFor( 249 final ModContainer mod, 250 final boolean ownDirectory, 251 final ConfigurationOptions options) throws ConfigurateException { 252 return fileWatcher().listenToConfiguration(path -> { 253 return HoconConfigurationLoader.builder() 254 .path(path) 255 .defaultOptions(options) 256 .build(); 257 }, configurationFile(mod, ownDirectory)); 258 } 259 260 /** 261 * Get the path to a configuration file in HOCON format for {@code mod}. 262 * 263 * <p>HOCON uses the {@code .conf} file extension. 264 * 265 * @param mod container of the mod 266 * @param ownDirectory whether the configuration should be in its own 267 * directory, or in the main configuration directory 268 * @return path to a configuration file 269 * @since 1.1.0 270 */ 271 public static Path configurationFile(final ModContainer mod, final boolean ownDirectory) { 272 Path configRoot = FabricLoader.getInstance().getConfigDir(); 273 if (ownDirectory) { 274 configRoot = configRoot.resolve(mod.getMetadata().getId()); 275 } 276 try { 277 Files.createDirectories(configRoot); 278 } catch (final IOException ignore) { 279 // we tried 280 } 281 return configRoot.resolve(mod.getMetadata().getId() + ".conf"); 282 } 283 284 /** 285 * Create a {@link ConfigurationTransformation} that applies a 286 * {@link DataFixer} to a Configurate node. The current version of the node 287 * is provided by the path {@code versionKey}. The transformation is 288 * executed from the provided node. 289 * 290 * @param fixer the fixer containing DFU transformations to apply 291 * @param reference the reference to the DFU {@link DSL} type representing 292 * this node 293 * @param targetVersion the version to convert to 294 * @param versionKey the location of the data version in nodes provided to 295 * the transformer 296 * @return a transformation that executes a {@link DataFixer data fixer}. 297 * @since 1.1.0 298 */ 299 public static ConfigurationTransformation createTransformation( 300 final DataFixer fixer, 301 final DSL.TypeReference reference, 302 final int targetVersion, 303 final Object... versionKey) { 304 return ConfigurationTransformation.builder() 305 .addAction(NodePath.path(), createTransformAction(fixer, reference, targetVersion, versionKey)) 306 .build(); 307 308 } 309 310 /** 311 * Create a TransformAction applying a {@link DataFixer} to a Configurate 312 * node. This can be used within {@link ConfigurationTransformation} 313 * when some values are controlled by DFUs and some aren't. 314 * 315 * @param fixer the fixer containing DFU transformations to apply 316 * @param reference the reference to the DFU {@link DSL} type representing this node 317 * @param targetVersion the version to convert to 318 * @param versionKey the location of the data version in nodes seen by 319 * this action. 320 * @return the created action 321 * @since 1.1.0 322 */ 323 public static TransformAction createTransformAction( 324 final DataFixer fixer, 325 final DSL.TypeReference reference, 326 final int targetVersion, 327 final Object... versionKey) { 328 return (inputPath, valueAtPath) -> { 329 final int currentVersion = valueAtPath.node(versionKey).getInt(-1); 330 final Dynamic<ConfigurationNode> dyn = ConfigurateOps.wrap(valueAtPath); 331 valueAtPath.set(fixer.update(reference, dyn, currentVersion, targetVersion).getValue()); 332 return null; 333 }; 334 } 335 336 /** 337 * Access the shared watch service for listening to files in this game on 338 * the default filesystem. 339 * 340 * @return watcher 341 * @since 1.1.0 342 */ 343 public static WatchServiceListener fileWatcher() { 344 final WatchServiceListener ret = Confabricate.listener; 345 if (ret == null) { 346 throw new IllegalStateException("Configurate file watcher failed to initialize, check log for earlier errors"); 347 } 348 return ret; 349 } 350 351 /** 352 * Return a builder pre-configured to apply Minecraft's DataFixers to the 353 * latest game save version. 354 * 355 * @return new transformation builder 356 * @since 2.0.0 357 */ 358 public static DataFixerTransformation.Builder minecraftDfuBuilder() { 359 return DataFixerTransformation.dfuBuilder() 360 .versionKey("minecraft-data-version") 361 .dataFixer(DataFixers.getDataFixer()) 362 // This seems to always be a bit higher than the latest declared schema. 363 // Don't know why, but the rest of the game uses this version. 364 .targetVersion(SharedConstants.getCurrentVersion().getDataVersion().getVersion()); 365 } 366 367}