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>&lt;config root&gt;/&lt;modid&gt;/&lt;modid&gt;.conf</pre>.
132     * Otherwise, the path will be
133     * <pre>&lt;config root&gt;/&lt;modid&gt;.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>&lt;config root&gt;/&lt;modid&gt;/&lt;modid&gt;.conf</pre>.
156     * Otherwise, the path will be
157     * <pre>&lt;config root&gt;/&lt;modid&gt;.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>&lt;config root&gt;/&lt;modid&gt;/&lt;modid&gt;.conf</pre>
206     * Otherwise, the path will be
207     * <pre>&lt;config root&gt;/&lt;modid&gt;.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>&lt;config root&gt;/&lt;modid&gt;/&lt;modid&gt;.conf</pre>
233     * Otherwise, the path will be
234     * <pre>&lt;config root&gt;/&lt;modid&gt;.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}