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>&lt;config root&gt;/&lt;modid&gt;/&lt;modid&gt;.conf</pre>.
131     * Otherwise, the path will be
132     * <pre>&lt;config root&gt;/&lt;modid&gt;.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>&lt;config root&gt;/&lt;modid&gt;/&lt;modid&gt;.conf</pre>.
155     * Otherwise, the path will be
156     * <pre>&lt;config root&gt;/&lt;modid&gt;.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>&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(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}