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 net.fabricmc.api.ModInitializer; 024import net.fabricmc.loader.api.FabricLoader; 025import net.fabricmc.loader.api.ModContainer; 026import net.minecraft.SharedConstants; 027import net.minecraft.datafixer.Schemas; 028import net.minecraft.util.Identifier; 029import org.apache.logging.log4j.LogManager; 030import org.apache.logging.log4j.Logger; 031import org.spongepowered.configurate.CommentedConfigurationNode; 032import org.spongepowered.configurate.ConfigurateException; 033import org.spongepowered.configurate.ConfigurationNode; 034import org.spongepowered.configurate.ConfigurationOptions; 035import org.spongepowered.configurate.NodePath; 036import org.spongepowered.configurate.extra.dfu.v4.ConfigurateOps; 037import org.spongepowered.configurate.extra.dfu.v4.DataFixerTransformation; 038import org.spongepowered.configurate.hocon.HoconConfigurationLoader; 039import org.spongepowered.configurate.loader.ConfigurationLoader; 040import org.spongepowered.configurate.reference.ConfigurationReference; 041import org.spongepowered.configurate.reference.WatchServiceListener; 042import org.spongepowered.configurate.transformation.ConfigurationTransformation; 043import org.spongepowered.configurate.transformation.TransformAction; 044 045import java.io.IOException; 046import java.nio.file.Files; 047import java.nio.file.Path; 048 049/** 050 * Configurate integration holder, providing access to configuration loaders 051 * pre-configured to work with Minecraft types. 052 * 053 * <p>This class has static utility methods for usage by other mods -- it should 054 * not be instantiated by anyone but the mod loader. 055 * 056 * @since 1.0.0 057 */ 058public class Confabricate implements ModInitializer { 059 060 static final String MOD_ID = "confabricate"; 061 062 static final Logger LOGGER = LogManager.getLogger(); 063 064 private static WatchServiceListener listener; 065 066 static { 067 try { 068 Confabricate.listener = WatchServiceListener.create(); 069 Runtime.getRuntime().addShutdownHook(new Thread(() -> { 070 try { 071 Confabricate.listener.close(); 072 } catch (final IOException e) { 073 LOGGER.catching(e); 074 } 075 }, "Confabricate shutdown thread")); 076 } catch (final IOException e) { 077 LOGGER.error("Could not initialize file listener", e); 078 } 079 } 080 081 /** 082 * Internal API to get a mod {@link Identifier}. 083 * 084 * @param item path value 085 * @return new identifier 086 * @since 2.0.0 087 */ 088 @RestrictedApi(explanation = "confabricate namespace is not open to others", 089 link = "", allowedOnPath = ".*/ca/stellardrift/confabricate/.*") 090 public static Identifier id(final String item) { 091 return new Identifier(MOD_ID, item); 092 } 093 094 @Override 095 public void onInitialize() { 096 // initialize serializers early, fail fast 097 MinecraftSerializers.collection(); 098 } 099 100 /** 101 * Get configuration options configured to use Confabricate's serializers. 102 * 103 * @return customized options 104 * @since 2.0.0 105 */ 106 public static ConfigurationOptions confabricateOptions() { 107 return ConfigurationOptions.defaults() 108 .serializers(MinecraftSerializers.collection()); 109 } 110 111 /** 112 * Create a configuration loader for the given mod's main 113 * configuration file. 114 * 115 * <p>By default, this config file is in a dedicated directory for the mod. 116 * 117 * @param mod the mod wanting to access its config 118 * @return a configuration loader in the Hocon format 119 * @see #loaderFor(ModContainer, boolean, ConfigurationOptions) 120 * @since 1.0.0 121 */ 122 public static ConfigurationLoader<CommentedConfigurationNode> loaderFor(final ModContainer mod) { 123 return loaderFor(mod, true); 124 } 125 126 /** 127 * Get a configuration loader for a mod. The configuration will be in 128 * Hocon format. 129 * 130 * <p>If the configuration is in its own directory, the path will be 131 * <pre><config root>/<modid>/<modid>.conf</pre>. 132 * Otherwise, the path will be 133 * <pre><config root>/<modid>.conf</pre>. 134 * 135 * <p>The returned {@link ConfigurationLoader ConfigurationLoaders} will be 136 * pre-configured to use the type serializers from 137 * {@link MinecraftSerializers#collection()}, but will otherwise use 138 * default settings. 139 * 140 * @param mod the mod to get the configuration loader for 141 * @param ownDirectory whether the configuration should be in a directory 142 * just for the mod, or a file in the config root 143 * @return the newly created configuration loader 144 * @since 1.0.0 145 */ 146 public static ConfigurationLoader<CommentedConfigurationNode> loaderFor(final ModContainer mod, final boolean ownDirectory) { 147 return loaderFor(mod, ownDirectory, confabricateOptions()); 148 } 149 150 /** 151 * Get a configuration loader for a mod. The configuration will be in 152 * Hocon format. 153 * 154 * <p>If the configuration is in its own directory, the path will be 155 * <pre><config root>/<modid>/<modid>.conf</pre>. 156 * Otherwise, the path will be 157 * <pre><config root>/<modid>.conf</pre>. 158 * 159 * <p>The returned {@link ConfigurationLoader ConfigurationLoaders} will be 160 * pre-configured to use the type serializers from 161 * {@link MinecraftSerializers#collection()}, but will otherwise use 162 * default settings. 163 * 164 * @param mod the mod to get the configuration loader for 165 * @param ownDirectory whether the configuration should be in a directory 166 * just for the mod, or a file in the config root 167 * @param options the options to use by default when loading 168 * @return the newly created configuration loader 169 * @since 2.0.0 170 */ 171 public static ConfigurationLoader<CommentedConfigurationNode> loaderFor( 172 final ModContainer mod, 173 final boolean ownDirectory, 174 final ConfigurationOptions options) { 175 return HoconConfigurationLoader.builder() 176 .path(configurationFile(mod, ownDirectory)) 177 .defaultOptions(options) 178 .build(); 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(Schemas.getFixer()) 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.getGameVersion().getWorldVersion()); 365 } 366 367}