diff --git a/src/main/java/net/fabricmc/loader/api/FabricLoader.java b/src/main/java/net/fabricmc/loader/api/FabricLoader.java
index e83b54033..106226dd0 100644
--- a/src/main/java/net/fabricmc/loader/api/FabricLoader.java
+++ b/src/main/java/net/fabricmc/loader/api/FabricLoader.java
@@ -236,4 +236,12 @@ static FabricLoader getInstance() {
* @return the launch arguments for the game
*/
String[] getLaunchArguments(boolean sanitize);
+
+ /**
+ * Get a {@link ModDirectories} instance, proving access to various cache or data directories.
+ *
+ * @param modId the ID of the mod.
+ * @return A {@link ModDirectories} instance.
+ */
+ ModDirectories getDirectories(String modId);
}
diff --git a/src/main/java/net/fabricmc/loader/api/ModDirectories.java b/src/main/java/net/fabricmc/loader/api/ModDirectories.java
new file mode 100644
index 000000000..881caf2fc
--- /dev/null
+++ b/src/main/java/net/fabricmc/loader/api/ModDirectories.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2016 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.loader.api;
+
+import java.nio.file.Path;
+
+/**
+ * Provides access to cache or data directories for a mod.
+ *
+ *
These directories are unique to the mod id and should not be shared between mods. These directories are not versioned so
+ * special care must be taken to ensure backwards compatibility with older versions of the mod running on the same machine.
+ *
+ *
Global directories are shared between all instances of Fabric Loader for the current user of the machine, no matter the game or launcher.
+ *
+ *
The cache directories should be used for temporary files that can be regenerated if lost.
+ *
+ *
Any sensitive data should be encrypted, and the private key stored in the {@link #getGlobalDataDir()}.
+ *
+ *
A sandbox implementation will grant full access to all of these directories, and may not isolate them from other instances.
+ * Code should not be stored and executed from these directories to prevent a sandbox escape.
+ */
+public interface ModDirectories {
+ /**
+ * Get the local cache directory for the mod.
+ *
+ *
Note: This directory should not be distributed, for example in a mod pack.
+ *
+ * @return A {@link Path} to the cache directory.
+ */
+ Path getCacheDir();
+
+ /**
+ * Get the global cache directory for the mod.
+ *
+ * @return A {@link Path} to the global cache directory.
+ */
+ Path getGlobalCacheDir();
+
+ /**
+ * Get the local data directory for the mod.
+ *
+ * @return A {@link Path} to the data directory.
+ */
+ Path getDataDir();
+
+ /**
+ * Get the global data directory for the mod.
+ *
+ * @return A {@link Path} to the global data directory.
+ */
+ Path getGlobalDataDir();
+}
diff --git a/src/main/java/net/fabricmc/loader/impl/FabricLoaderImpl.java b/src/main/java/net/fabricmc/loader/impl/FabricLoaderImpl.java
index 0977d60c2..bd5e447fe 100644
--- a/src/main/java/net/fabricmc/loader/impl/FabricLoaderImpl.java
+++ b/src/main/java/net/fabricmc/loader/impl/FabricLoaderImpl.java
@@ -43,6 +43,7 @@
import net.fabricmc.loader.api.LanguageAdapter;
import net.fabricmc.loader.api.MappingResolver;
import net.fabricmc.loader.api.ModContainer;
+import net.fabricmc.loader.api.ModDirectories;
import net.fabricmc.loader.api.ObjectShare;
import net.fabricmc.loader.api.entrypoint.EntrypointContainer;
import net.fabricmc.loader.impl.discovery.ArgumentModCandidateFinder;
@@ -60,10 +61,13 @@
import net.fabricmc.loader.impl.metadata.DependencyOverrides;
import net.fabricmc.loader.impl.metadata.EntrypointMetadata;
import net.fabricmc.loader.impl.metadata.LoaderModMetadata;
+import net.fabricmc.loader.impl.metadata.MetadataVerifier;
import net.fabricmc.loader.impl.metadata.VersionOverrides;
import net.fabricmc.loader.impl.util.DefaultLanguageAdapter;
import net.fabricmc.loader.impl.util.ExceptionUtil;
+import net.fabricmc.loader.impl.util.GlobalDirectories;
import net.fabricmc.loader.impl.util.LoaderUtil;
+import net.fabricmc.loader.impl.util.ModDirectoriesImpl;
import net.fabricmc.loader.impl.util.SystemProperties;
import net.fabricmc.loader.impl.util.log.Log;
import net.fabricmc.loader.impl.util.log.LogCategory;
@@ -77,7 +81,6 @@ public final class FabricLoaderImpl extends net.fabricmc.loader.FabricLoader {
public static final String VERSION = "0.15.11";
public static final String MOD_ID = "fabricloader";
- public static final String CACHE_DIR_NAME = ".fabric"; // relative to game dir
private static final String PROCESSED_MODS_DIR_NAME = "processedMods"; // relative to cache dir
public static final String REMAPPED_JARS_DIR_NAME = "remappedJars"; // relative to cache dir
private static final String TMP_DIR_NAME = "tmp"; // relative to cache dir
@@ -100,6 +103,7 @@ public final class FabricLoaderImpl extends net.fabricmc.loader.FabricLoader {
private GameProvider provider;
private Path gameDir;
private Path configDir;
+ private GlobalDirectories globalDirectories;
private FabricLoaderImpl() { }
@@ -134,6 +138,7 @@ public void setGameProvider(GameProvider provider) {
private void setGameDir(Path gameDir) {
this.gameDir = gameDir;
this.configDir = gameDir.resolve("config");
+ this.globalDirectories = GlobalDirectories.create(getEnvironmentType(), gameDir);
}
@Override
@@ -230,7 +235,7 @@ private void setup() throws ModResolutionException {
dumpModList(modCandidates);
- Path cacheDir = gameDir.resolve(CACHE_DIR_NAME);
+ Path cacheDir = getDirectories(MOD_ID).getCacheDir();
Path outputdir = cacheDir.resolve(PROCESSED_MODS_DIR_NAME);
// runtime mod remapping
@@ -595,6 +600,15 @@ public String[] getLaunchArguments(boolean sanitize) {
return getGameProvider().getLaunchArguments(sanitize);
}
+ @Override
+ public ModDirectories getDirectories(String modId) {
+ if (!MetadataVerifier.isValidModId(modId)) {
+ throw new IllegalArgumentException("Invalid mod ID: " + modId);
+ }
+
+ return new ModDirectoriesImpl(modId, getGameDir(), globalDirectories);
+ }
+
@Override
protected Path getModsDirectory0() {
String directory = System.getProperty(SystemProperties.MODS_FOLDER);
diff --git a/src/main/java/net/fabricmc/loader/impl/game/GameProviderHelper.java b/src/main/java/net/fabricmc/loader/impl/game/GameProviderHelper.java
index 0e724ef1c..46a8bcd45 100644
--- a/src/main/java/net/fabricmc/loader/impl/game/GameProviderHelper.java
+++ b/src/main/java/net/fabricmc/loader/impl/game/GameProviderHelper.java
@@ -189,7 +189,7 @@ public static Map deobfuscate(Map inputFileMap, Stri
return inputFileMap;
}
- Path deobfJarDir = getDeobfJarDir(gameDir, gameId, gameVersion);
+ Path deobfJarDir = getDeobfJarDir(gameId, gameVersion);
List inputFiles = new ArrayList<>(inputFileMap.size());
List outputFiles = new ArrayList<>(inputFileMap.size());
List tmpFiles = new ArrayList<>(inputFileMap.size());
@@ -247,8 +247,8 @@ public static Map deobfuscate(Map inputFileMap, Stri
return ret;
}
- private static Path getDeobfJarDir(Path gameDir, String gameId, String gameVersion) {
- Path ret = gameDir.resolve(FabricLoaderImpl.CACHE_DIR_NAME).resolve(FabricLoaderImpl.REMAPPED_JARS_DIR_NAME);
+ private static Path getDeobfJarDir(String gameId, String gameVersion) {
+ Path ret = FabricLoaderImpl.INSTANCE.getDirectories(FabricLoaderImpl.MOD_ID).getCacheDir().resolve(FabricLoaderImpl.REMAPPED_JARS_DIR_NAME);
StringBuilder versionDirName = new StringBuilder();
if (!gameId.isEmpty()) {
diff --git a/src/main/java/net/fabricmc/loader/impl/metadata/MetadataVerifier.java b/src/main/java/net/fabricmc/loader/impl/metadata/MetadataVerifier.java
index c0157c109..1799e61a2 100644
--- a/src/main/java/net/fabricmc/loader/impl/metadata/MetadataVerifier.java
+++ b/src/main/java/net/fabricmc/loader/impl/metadata/MetadataVerifier.java
@@ -22,6 +22,7 @@
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
+import java.util.Objects;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@@ -145,4 +146,9 @@ private static void checkModId(String id, String name) throws ParseMetadataExcep
throw new ParseMetadataException(sw.toString());
}
+
+ public static boolean isValidModId(String id) {
+ Objects.requireNonNull(id, "id");
+ return MOD_ID_PATTERN.matcher(id).matches();
+ }
}
diff --git a/src/main/java/net/fabricmc/loader/impl/util/GlobalDirectories.java b/src/main/java/net/fabricmc/loader/impl/util/GlobalDirectories.java
new file mode 100644
index 000000000..b94c054a6
--- /dev/null
+++ b/src/main/java/net/fabricmc/loader/impl/util/GlobalDirectories.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright 2016 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.loader.impl.util;
+
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Locale;
+
+import net.fabricmc.api.EnvType;
+
+public abstract class GlobalDirectories {
+ abstract Path getGlobalCacheRoot();
+
+ abstract Path getGlobalDataRoot();
+
+ public static GlobalDirectories create(EnvType envType, Path gameDir) {
+ switch (envType) {
+ case CLIENT:
+ return Client.create();
+ case SERVER:
+ return new Server(gameDir);
+ default:
+ throw new IllegalStateException();
+ }
+ }
+
+ abstract static class Client extends GlobalDirectories {
+ private final Path cache;
+ private final Path data;
+
+ protected Client(Path cache, Path data) {
+ this.cache = cache;
+ this.data = data;
+ }
+
+ @Override
+ public Path getGlobalCacheRoot() {
+ return cache;
+ }
+
+ @Override
+ public Path getGlobalDataRoot() {
+ return data;
+ }
+
+ static Client create() {
+ final String os = System.getProperty("os.name").toLowerCase(Locale.ROOT);
+
+ if (os.contains("win")) {
+ return new Windows();
+ } else if (os.contains("mac")) {
+ return new MacOS();
+ }
+
+ // Linux or unknown.
+ return new Linux();
+ }
+
+ static final class Windows extends Client {
+ private Windows() {
+ super(getCacheDir(), getDataDir());
+ }
+
+ private static Path getCacheDir() {
+ return Paths.get(System.getenv("LocalAppData"), "net.fabricmc.loader");
+ }
+
+ private static Path getDataDir() {
+ return Paths.get(System.getenv("AppData"), "net.fabricmc.loader");
+ }
+ }
+
+ static final class MacOS extends Client {
+ private MacOS() {
+ super(getCacheDir(), getDataDir());
+ }
+
+ private static Path getCacheDir() {
+ return Paths.get(System.getProperty("user.home"), "Library", "Caches", "net.fabricmc.loader");
+ }
+
+ private static Path getDataDir() {
+ return Paths.get(System.getProperty("user.home"), "Library", "Application Support", "net.fabricmc.loader");
+ }
+ }
+
+ static final class Linux extends Client {
+ private Linux() {
+ super(getCacheDir(), getDataDir());
+ }
+
+ private static Path getCacheDir() {
+ return Paths.get(System.getProperty("user.home"), ".caches", "net.fabricmc.loader");
+ }
+
+ private static Path getDataDir() {
+ return Paths.get(System.getProperty("user.home"), ".config", "net.fabricmc.loader");
+ }
+ }
+ }
+
+ static final class Server extends GlobalDirectories {
+ private final Path gameDir;
+
+ private Server(Path gameDir) {
+ this.gameDir = gameDir;
+ }
+
+ @Override
+ public Path getGlobalCacheRoot() {
+ return gameDir.resolve("fabric-global");
+ }
+
+ @Override
+ public Path getGlobalDataRoot() {
+ return gameDir.resolve("cache-global");
+ }
+ }
+}
diff --git a/src/main/java/net/fabricmc/loader/impl/util/ModDirectoriesImpl.java b/src/main/java/net/fabricmc/loader/impl/util/ModDirectoriesImpl.java
new file mode 100644
index 000000000..4ffcb4c6c
--- /dev/null
+++ b/src/main/java/net/fabricmc/loader/impl/util/ModDirectoriesImpl.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2016 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.loader.impl.util;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Objects;
+
+import net.fabricmc.loader.api.ModDirectories;
+
+public final class ModDirectoriesImpl implements ModDirectories {
+ private final String modId;
+ private final Path gameDir;
+ private final GlobalDirectories globalDirectories;
+
+ public ModDirectoriesImpl(String modId, Path gameDir, GlobalDirectories globalDirectories) {
+ this.modId = Objects.requireNonNull(modId);
+ this.gameDir = gameDir;
+ this.globalDirectories = Objects.requireNonNull(globalDirectories);
+ }
+
+ @Override
+ public Path getCacheDir() {
+ return ensureDir(gameDir.resolve("cache").resolve(modId));
+ }
+
+ @Override
+ public Path getGlobalCacheDir() {
+ return ensureDir(globalDirectories.getGlobalCacheRoot().resolve(modId));
+ }
+
+ @Override
+ public Path getDataDir() {
+ return ensureDir(gameDir.resolve("fabric").resolve(modId));
+ }
+
+ @Override
+ public Path getGlobalDataDir() {
+ return ensureDir(globalDirectories.getGlobalDataRoot().resolve(modId));
+ }
+
+ private Path ensureDir(Path path) {
+ if (!Files.exists(path)) {
+ try {
+ Files.createDirectories(path);
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to create directory: " + path, e);
+ }
+ }
+
+ return path;
+ }
+}
diff --git a/src/test/java/net/fabricmc/test/ModDirectoriesTests.java b/src/test/java/net/fabricmc/test/ModDirectoriesTests.java
new file mode 100644
index 000000000..2fe87bcc9
--- /dev/null
+++ b/src/test/java/net/fabricmc/test/ModDirectoriesTests.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2016 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.test;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import org.junit.jupiter.api.Test;
+
+import net.fabricmc.api.EnvType;
+import net.fabricmc.loader.api.ModDirectories;
+import net.fabricmc.loader.impl.util.GlobalDirectories;
+import net.fabricmc.loader.impl.util.ModDirectoriesImpl;
+
+// Just a basic test to ensure that we can write to these directories
+public class ModDirectoriesTests {
+ private final byte[] DATA = "Hello World".getBytes(StandardCharsets.UTF_8);
+
+ @Test
+ void testModDirectoriesClient() throws IOException {
+ Path gameDir = Files.createTempDirectory("loader-test");
+ GlobalDirectories globalDirectories = GlobalDirectories.create(EnvType.CLIENT, gameDir);
+ ModDirectories directories = new ModDirectoriesImpl("test", gameDir, globalDirectories);
+
+ Files.write(directories.getCacheDir().resolve("test.txt"), DATA);
+ Files.write(directories.getGlobalCacheDir().resolve("test.txt"), DATA);
+ Files.write(directories.getDataDir().resolve("test.txt"), DATA);
+ Files.write(directories.getGlobalDataDir().resolve("test.txt"), DATA);
+ }
+
+ @Test
+ void testModDirectoriesServer() throws IOException {
+ Path gameDir = Files.createTempDirectory("loader-test");
+ GlobalDirectories globalDirectories = GlobalDirectories.create(EnvType.SERVER, gameDir);
+ ModDirectories directories = new ModDirectoriesImpl("test", gameDir, globalDirectories);
+
+ Files.write(directories.getCacheDir().resolve("test.txt"), DATA);
+ Files.write(directories.getGlobalCacheDir().resolve("test.txt"), DATA);
+ Files.write(directories.getDataDir().resolve("test.txt"), DATA);
+ Files.write(directories.getGlobalDataDir().resolve("test.txt"), DATA);
+ }
+}