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>&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     * 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>&lt;config root&gt;/&lt;modid&gt;/&lt;modid&gt;.conf</pre>
205     * Otherwise, the path will be
206     * <pre>&lt;config root&gt;/&lt;modid&gt;.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>&lt;config root&gt;/&lt;modid&gt;/&lt;modid&gt;.conf</pre>
232     * Otherwise, the path will be
233     * <pre>&lt;config root&gt;/&lt;modid&gt;.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}