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.resources.ResourceLocation; 028import net.minecraft.util.datafix.DataFixers; 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 ResourceLocation}. 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 ResourceLocation id(final String item) { 091 return new ResourceLocation(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 /** 183 * Create a configuration reference to the provided mod's main 184 * configuration file. 185 * 186 * <p>By default, this config file is in a dedicated directory for the mod. 187 * The returned reference will automatically reload. 188 * 189 * @param mod the mod wanting to access its config 190 * @return a configuration reference for a loaded node in HOCON format 191 * @throws ConfigurateException if a listener could not be established or if 192 * the configuration failed to load. 193 * @see #configurationFor(ModContainer, boolean, ConfigurationOptions) 194 * @since 1.1.0 195 */ 196 public static ConfigurationReference<CommentedConfigurationNode> configurationFor(final ModContainer mod) throws ConfigurateException { 197 return configurationFor(mod, true); 198 } 199 200 /** 201 * Get a configuration reference for a mod. The configuration will be in 202 * Hocon format. 203 * 204 * <p>If the configuration is in its own directory, the path will be 205 * <pre><config root>/<modid>/<modid>.conf</pre> 206 * Otherwise, the path will be 207 * <pre><config root>/<modid>.conf</pre>. 208 * 209 * <p>The reference's {@link ConfigurationLoader} will be pre-configured to 210 * use the type serializers from {@link MinecraftSerializers#collection()} 211 * but will otherwise use default settings. 212 * 213 * @param mod the mod to get the configuration loader for 214 * @param ownDirectory whether the configuration should be in a directory 215 * just for the mod 216 * @return the newly created and loaded configuration reference 217 * @throws ConfigurateException if a listener could not be established or 218 * the configuration failed to load. 219 * @since 1.1.0 220 */ 221 public static ConfigurationReference<CommentedConfigurationNode> configurationFor( 222 final ModContainer mod, 223 final boolean ownDirectory) throws ConfigurateException { 224 return configurationFor(mod, ownDirectory, confabricateOptions()); 225 } 226 227 /** 228 * Get a configuration reference for a mod. The configuration will be in 229 * Hocon format. 230 * 231 * <p>If the configuration is in its own directory, the path will be 232 * <pre><config root>/<modid>/<modid>.conf</pre> 233 * Otherwise, the path will be 234 * <pre><config root>/<modid>.conf</pre>. 235 * 236 * <p>The reference's {@link ConfigurationLoader} will be pre-configured to 237 * use the type serializers from {@link MinecraftSerializers#collection()} 238 * but will otherwise use default settings. 239 * 240 * @param mod the mod to get the configuration loader for 241 * @param ownDirectory whether the configuration should be in a directory 242 * just for the mod 243 * @param options the options to use by default when loading 244 * @return the newly created and loaded configuration reference 245 * @throws ConfigurateException if a listener could not be established or 246 * the configuration failed to load. 247 * @since 2.0.0 248 */ 249 public static ConfigurationReference<CommentedConfigurationNode> configurationFor( 250 final ModContainer mod, 251 final boolean ownDirectory, 252 final ConfigurationOptions options) throws ConfigurateException { 253 return fileWatcher().listenToConfiguration(path -> { 254 return HoconConfigurationLoader.builder() 255 .path(path) 256 .defaultOptions(options) 257 .build(); 258 }, configurationFile(mod, ownDirectory)); 259 } 260 261 /** 262 * Get the path to a configuration file in HOCON format for {@code mod}. 263 * 264 * <p>HOCON uses the {@code .conf} file extension. 265 * 266 * @param mod container of the mod 267 * @param ownDirectory whether the configuration should be in its own 268 * directory, or in the main configuration directory 269 * @return path to a configuration file 270 * @since 1.1.0 271 */ 272 public static Path configurationFile(final ModContainer mod, final boolean ownDirectory) { 273 Path configRoot = FabricLoader.getInstance().getConfigDir(); 274 if (ownDirectory) { 275 configRoot = configRoot.resolve(mod.getMetadata().getId()); 276 } 277 try { 278 Files.createDirectories(configRoot); 279 } catch (final IOException ignore) { 280 // we tried 281 } 282 return configRoot.resolve(mod.getMetadata().getId() + ".conf"); 283 } 284 285 /** 286 * Create a {@link ConfigurationTransformation} that applies a 287 * {@link DataFixer} to a Configurate node. The current version of the node 288 * is provided by the path {@code versionKey}. The transformation is 289 * executed from the provided node. 290 * 291 * @param fixer the fixer containing DFU transformations to apply 292 * @param reference the reference to the DFU {@link DSL} type representing 293 * this node 294 * @param targetVersion the version to convert to 295 * @param versionKey the location of the data version in nodes provided to 296 * the transformer 297 * @return a transformation that executes a {@link DataFixer data fixer}. 298 * @since 1.1.0 299 */ 300 public static ConfigurationTransformation createTransformation( 301 final DataFixer fixer, 302 final DSL.TypeReference reference, 303 final int targetVersion, 304 final Object... versionKey) { 305 return ConfigurationTransformation.builder() 306 .addAction(NodePath.path(), createTransformAction(fixer, reference, targetVersion, versionKey)) 307 .build(); 308 309 } 310 311 /** 312 * Create a TransformAction applying a {@link DataFixer} to a Configurate 313 * node. This can be used within {@link ConfigurationTransformation} 314 * when some values are controlled by DFUs and some aren't. 315 * 316 * @param fixer the fixer containing DFU transformations to apply 317 * @param reference the reference to the DFU {@link DSL} type representing this node 318 * @param targetVersion the version to convert to 319 * @param versionKey the location of the data version in nodes seen by 320 * this action. 321 * @return the created action 322 * @since 1.1.0 323 */ 324 public static TransformAction createTransformAction( 325 final DataFixer fixer, 326 final DSL.TypeReference reference, 327 final int targetVersion, 328 final Object... versionKey) { 329 return (inputPath, valueAtPath) -> { 330 final int currentVersion = valueAtPath.node(versionKey).getInt(-1); 331 final Dynamic<ConfigurationNode> dyn = ConfigurateOps.wrap(valueAtPath); 332 valueAtPath.set(fixer.update(reference, dyn, currentVersion, targetVersion).getValue()); 333 return null; 334 }; 335 } 336 337 /** 338 * Access the shared watch service for listening to files in this game on 339 * the default filesystem. 340 * 341 * @return watcher 342 * @since 1.1.0 343 */ 344 public static WatchServiceListener fileWatcher() { 345 final WatchServiceListener ret = Confabricate.listener; 346 if (ret == null) { 347 throw new IllegalStateException("Configurate file watcher failed to initialize, check log for earlier errors"); 348 } 349 return ret; 350 } 351 352 /** 353 * Return a builder pre-configured to apply Minecraft's DataFixers to the 354 * latest game save version. 355 * 356 * @return new transformation builder 357 * @since 2.0.0 358 */ 359 public static DataFixerTransformation.Builder minecraftDfuBuilder() { 360 return DataFixerTransformation.dfuBuilder() 361 .versionKey("minecraft-data-version") 362 .dataFixer(DataFixers.getDataFixer()) 363 // This seems to always be a bit higher than the latest declared schema. 364 // Don't know why, but the rest of the game uses this version. 365 .targetVersion(SharedConstants.getCurrentVersion().getDataVersion().getVersion()); 366 } 367 368}