298 lines
		
	
	
	
		
			9.4 KiB
		
	
	
	
		
			Java
		
	
	
	
	
	
			
		
		
	
	
			298 lines
		
	
	
	
		
			9.4 KiB
		
	
	
	
		
			Java
		
	
	
	
	
	
| package net.minecraft.data;
 | |
| 
 | |
| import com.google.common.collect.ImmutableMap;
 | |
| import com.google.common.collect.ImmutableMap.Builder;
 | |
| import com.google.common.hash.HashCode;
 | |
| import com.google.common.hash.Hashing;
 | |
| import com.mojang.logging.LogUtils;
 | |
| import java.io.BufferedReader;
 | |
| import java.io.BufferedWriter;
 | |
| import java.io.IOException;
 | |
| import java.nio.charset.StandardCharsets;
 | |
| import java.nio.file.FileVisitResult;
 | |
| import java.nio.file.Files;
 | |
| import java.nio.file.LinkOption;
 | |
| import java.nio.file.OpenOption;
 | |
| import java.nio.file.Path;
 | |
| import java.nio.file.SimpleFileVisitor;
 | |
| import java.nio.file.attribute.BasicFileAttributes;
 | |
| import java.time.LocalDateTime;
 | |
| import java.time.format.DateTimeFormatter;
 | |
| import java.util.Collection;
 | |
| import java.util.HashMap;
 | |
| import java.util.HashSet;
 | |
| import java.util.Map;
 | |
| import java.util.Objects;
 | |
| import java.util.Set;
 | |
| import java.util.Map.Entry;
 | |
| import java.util.concurrent.CompletableFuture;
 | |
| import java.util.concurrent.ConcurrentHashMap;
 | |
| import java.util.concurrent.ConcurrentMap;
 | |
| import java.util.concurrent.atomic.AtomicInteger;
 | |
| import net.minecraft.WorldVersion;
 | |
| import org.apache.commons.lang3.mutable.MutableInt;
 | |
| import org.jetbrains.annotations.Nullable;
 | |
| import org.slf4j.Logger;
 | |
| 
 | |
| public class HashCache {
 | |
| 	static final Logger LOGGER = LogUtils.getLogger();
 | |
| 	private static final String HEADER_MARKER = "// ";
 | |
| 	private final Path rootDir;
 | |
| 	private final Path cacheDir;
 | |
| 	private final String versionId;
 | |
| 	private final Map<String, HashCache.ProviderCache> caches;
 | |
| 	private final Set<String> cachesToWrite = new HashSet();
 | |
| 	final Set<Path> cachePaths = new HashSet();
 | |
| 	private final int initialCount;
 | |
| 	private int writes;
 | |
| 
 | |
| 	private Path getProviderCachePath(String provider) {
 | |
| 		return this.cacheDir.resolve(Hashing.sha1().hashString(provider, StandardCharsets.UTF_8).toString());
 | |
| 	}
 | |
| 
 | |
| 	public HashCache(Path rootDir, Collection<String> providers, WorldVersion version) throws IOException {
 | |
| 		this.versionId = version.name();
 | |
| 		this.rootDir = rootDir;
 | |
| 		this.cacheDir = rootDir.resolve(".cache");
 | |
| 		Files.createDirectories(this.cacheDir);
 | |
| 		Map<String, HashCache.ProviderCache> map = new HashMap();
 | |
| 		int i = 0;
 | |
| 
 | |
| 		for (String string : providers) {
 | |
| 			Path path = this.getProviderCachePath(string);
 | |
| 			this.cachePaths.add(path);
 | |
| 			HashCache.ProviderCache providerCache = readCache(rootDir, path);
 | |
| 			map.put(string, providerCache);
 | |
| 			i += providerCache.count();
 | |
| 		}
 | |
| 
 | |
| 		this.caches = map;
 | |
| 		this.initialCount = i;
 | |
| 	}
 | |
| 
 | |
| 	private static HashCache.ProviderCache readCache(Path rootDir, Path cachePath) {
 | |
| 		if (Files.isReadable(cachePath)) {
 | |
| 			try {
 | |
| 				return HashCache.ProviderCache.load(rootDir, cachePath);
 | |
| 			} catch (Exception var3) {
 | |
| 				LOGGER.warn("Failed to parse cache {}, discarding", cachePath, var3);
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		return new HashCache.ProviderCache("unknown", ImmutableMap.of());
 | |
| 	}
 | |
| 
 | |
| 	public boolean shouldRunInThisVersion(String provider) {
 | |
| 		HashCache.ProviderCache providerCache = (HashCache.ProviderCache)this.caches.get(provider);
 | |
| 		return providerCache == null || !providerCache.version.equals(this.versionId);
 | |
| 	}
 | |
| 
 | |
| 	public CompletableFuture<HashCache.UpdateResult> generateUpdate(String provider, HashCache.UpdateFunction updateFunction) {
 | |
| 		HashCache.ProviderCache providerCache = (HashCache.ProviderCache)this.caches.get(provider);
 | |
| 		if (providerCache == null) {
 | |
| 			throw new IllegalStateException("Provider not registered: " + provider);
 | |
| 		} else {
 | |
| 			HashCache.CacheUpdater cacheUpdater = new HashCache.CacheUpdater(provider, this.versionId, providerCache);
 | |
| 			return updateFunction.update(cacheUpdater).thenApply(object -> cacheUpdater.close());
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	public void applyUpdate(HashCache.UpdateResult updateResult) {
 | |
| 		this.caches.put(updateResult.providerId(), updateResult.cache());
 | |
| 		this.cachesToWrite.add(updateResult.providerId());
 | |
| 		this.writes = this.writes + updateResult.writes();
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Writes the cache file containing the hashes of newly created files to the disk, and deletes any stale files.
 | |
| 	 */
 | |
| 	public void purgeStaleAndWrite() throws IOException {
 | |
| 		final Set<Path> set = new HashSet();
 | |
| 		this.caches.forEach((string, providerCache) -> {
 | |
| 			if (this.cachesToWrite.contains(string)) {
 | |
| 				Path path = this.getProviderCachePath(string);
 | |
| 				providerCache.save(this.rootDir, path, DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(LocalDateTime.now()) + "\t" + string);
 | |
| 			}
 | |
| 
 | |
| 			set.addAll(providerCache.data().keySet());
 | |
| 		});
 | |
| 		set.add(this.rootDir.resolve("version.json"));
 | |
| 		final MutableInt mutableInt = new MutableInt();
 | |
| 		final MutableInt mutableInt2 = new MutableInt();
 | |
| 		Files.walkFileTree(this.rootDir, new SimpleFileVisitor<Path>() {
 | |
| 			public FileVisitResult visitFile(Path path, BasicFileAttributes basicFileAttributes) {
 | |
| 				if (HashCache.this.cachePaths.contains(path)) {
 | |
| 					return FileVisitResult.CONTINUE;
 | |
| 				} else {
 | |
| 					mutableInt.increment();
 | |
| 					if (set.contains(path)) {
 | |
| 						return FileVisitResult.CONTINUE;
 | |
| 					} else {
 | |
| 						try {
 | |
| 							Files.delete(path);
 | |
| 						} catch (IOException var4) {
 | |
| 							HashCache.LOGGER.warn("Failed to delete file {}", path, var4);
 | |
| 						}
 | |
| 
 | |
| 						mutableInt2.increment();
 | |
| 						return FileVisitResult.CONTINUE;
 | |
| 					}
 | |
| 				}
 | |
| 			}
 | |
| 		});
 | |
| 		LOGGER.info(
 | |
| 			"Caching: total files: {}, old count: {}, new count: {}, removed stale: {}, written: {}",
 | |
| 			mutableInt,
 | |
| 			this.initialCount,
 | |
| 			set.size(),
 | |
| 			mutableInt2,
 | |
| 			this.writes
 | |
| 		);
 | |
| 	}
 | |
| 
 | |
| 	static class CacheUpdater implements CachedOutput {
 | |
| 		private final String provider;
 | |
| 		private final HashCache.ProviderCache oldCache;
 | |
| 		private final HashCache.ProviderCacheBuilder newCache;
 | |
| 		private final AtomicInteger writes = new AtomicInteger();
 | |
| 		private volatile boolean closed;
 | |
| 
 | |
| 		CacheUpdater(String provider, String version, HashCache.ProviderCache oldCache) {
 | |
| 			this.provider = provider;
 | |
| 			this.oldCache = oldCache;
 | |
| 			this.newCache = new HashCache.ProviderCacheBuilder(version);
 | |
| 		}
 | |
| 
 | |
| 		private boolean shouldWrite(Path key, HashCode value) {
 | |
| 			return !Objects.equals(this.oldCache.get(key), value) || !Files.exists(key, new LinkOption[0]);
 | |
| 		}
 | |
| 
 | |
| 		@Override
 | |
| 		public void writeIfNeeded(Path path, byte[] bs, HashCode hashCode) throws IOException {
 | |
| 			if (this.closed) {
 | |
| 				throw new IllegalStateException("Cannot write to cache as it has already been closed");
 | |
| 			} else {
 | |
| 				if (this.shouldWrite(path, hashCode)) {
 | |
| 					this.writes.incrementAndGet();
 | |
| 					Files.createDirectories(path.getParent());
 | |
| 					Files.write(path, bs, new OpenOption[0]);
 | |
| 				}
 | |
| 
 | |
| 				this.newCache.put(path, hashCode);
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		public HashCache.UpdateResult close() {
 | |
| 			this.closed = true;
 | |
| 			return new HashCache.UpdateResult(this.provider, this.newCache.build(), this.writes.get());
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	record ProviderCache(String version, ImmutableMap<Path, HashCode> data) {
 | |
| 
 | |
| 		@Nullable
 | |
| 		public HashCode get(Path path) {
 | |
| 			return this.data.get(path);
 | |
| 		}
 | |
| 
 | |
| 		public int count() {
 | |
| 			return this.data.size();
 | |
| 		}
 | |
| 
 | |
| 		public static HashCache.ProviderCache load(Path rootDir, Path cachePath) throws IOException {
 | |
| 			BufferedReader bufferedReader = Files.newBufferedReader(cachePath, StandardCharsets.UTF_8);
 | |
| 
 | |
| 			HashCache.ProviderCache var7;
 | |
| 			try {
 | |
| 				String string = bufferedReader.readLine();
 | |
| 				if (!string.startsWith("// ")) {
 | |
| 					throw new IllegalStateException("Missing cache file header");
 | |
| 				}
 | |
| 
 | |
| 				String[] strings = string.substring("// ".length()).split("\t", 2);
 | |
| 				String string2 = strings[0];
 | |
| 				Builder<Path, HashCode> builder = ImmutableMap.builder();
 | |
| 				bufferedReader.lines().forEach(stringx -> {
 | |
| 					int i = stringx.indexOf(32);
 | |
| 					builder.put(rootDir.resolve(stringx.substring(i + 1)), HashCode.fromString(stringx.substring(0, i)));
 | |
| 				});
 | |
| 				var7 = new HashCache.ProviderCache(string2, builder.build());
 | |
| 			} catch (Throwable var9) {
 | |
| 				if (bufferedReader != null) {
 | |
| 					try {
 | |
| 						bufferedReader.close();
 | |
| 					} catch (Throwable var8) {
 | |
| 						var9.addSuppressed(var8);
 | |
| 					}
 | |
| 				}
 | |
| 
 | |
| 				throw var9;
 | |
| 			}
 | |
| 
 | |
| 			if (bufferedReader != null) {
 | |
| 				bufferedReader.close();
 | |
| 			}
 | |
| 
 | |
| 			return var7;
 | |
| 		}
 | |
| 
 | |
| 		public void save(Path rootDir, Path cachePath, String date) {
 | |
| 			try {
 | |
| 				BufferedWriter bufferedWriter = Files.newBufferedWriter(cachePath, StandardCharsets.UTF_8);
 | |
| 
 | |
| 				try {
 | |
| 					bufferedWriter.write("// ");
 | |
| 					bufferedWriter.write(this.version);
 | |
| 					bufferedWriter.write(9);
 | |
| 					bufferedWriter.write(date);
 | |
| 					bufferedWriter.newLine();
 | |
| 
 | |
| 					for (Entry<Path, HashCode> entry : this.data.entrySet()) {
 | |
| 						bufferedWriter.write(((HashCode)entry.getValue()).toString());
 | |
| 						bufferedWriter.write(32);
 | |
| 						bufferedWriter.write(rootDir.relativize((Path)entry.getKey()).toString());
 | |
| 						bufferedWriter.newLine();
 | |
| 					}
 | |
| 				} catch (Throwable var8) {
 | |
| 					if (bufferedWriter != null) {
 | |
| 						try {
 | |
| 							bufferedWriter.close();
 | |
| 						} catch (Throwable var7) {
 | |
| 							var8.addSuppressed(var7);
 | |
| 						}
 | |
| 					}
 | |
| 
 | |
| 					throw var8;
 | |
| 				}
 | |
| 
 | |
| 				if (bufferedWriter != null) {
 | |
| 					bufferedWriter.close();
 | |
| 				}
 | |
| 			} catch (IOException var9) {
 | |
| 				HashCache.LOGGER.warn("Unable write cachefile {}: {}", cachePath, var9);
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	record ProviderCacheBuilder(String version, ConcurrentMap<Path, HashCode> data) {
 | |
| 		ProviderCacheBuilder(String version) {
 | |
| 			this(version, new ConcurrentHashMap());
 | |
| 		}
 | |
| 
 | |
| 		public void put(Path key, HashCode value) {
 | |
| 			this.data.put(key, value);
 | |
| 		}
 | |
| 
 | |
| 		public HashCache.ProviderCache build() {
 | |
| 			return new HashCache.ProviderCache(this.version, ImmutableMap.copyOf(this.data));
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	@FunctionalInterface
 | |
| 	public interface UpdateFunction {
 | |
| 		CompletableFuture<?> update(CachedOutput cachedOutput);
 | |
| 	}
 | |
| 
 | |
| 	public record UpdateResult(String providerId, HashCache.ProviderCache cache, int writes) {
 | |
| 	}
 | |
| }
 |