/*
* Copyright (c) Forge Development LLC and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/
package net.minecraftforge.event;
import net.minecraft.world.level.storage.LevelStorageSource;
import net.minecraftforge.eventbus.api.bus.BusGroup;
import net.minecraftforge.eventbus.api.bus.EventBus;
import net.minecraftforge.fml.LogicalSide;
import net.minecraftforge.fml.ModContainer;
import net.minecraftforge.fml.ModList;
import net.minecraftforge.fml.ModLoadingContext;
import net.minecraftforge.fml.event.IModBusEvent;
import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
import net.minecraftforge.forgespi.language.IModInfo;
import org.apache.maven.artifact.versioning.ArtifactVersion;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Nullable;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Stream;
/**
* Fires when the mod loader is in the process of loading a world that was last saved
* with mod versions that differ from the currently-loaded versions. This can be used to
* enqueue work to run at a later point, such as multi-file migration of data.
*
*
* Note that level and world information has not yet been fully loaded; as such, it is
* unsafe to access server or level information during handling of this event.
*
*
* This event is not {@linkplain Cancelable cancellable}, and does not {@linkplain HasResult have a result}.
* This event is fired on the {@linkplain FMLJavaModLoadingContext#getModEventBus() mod-specific event bus},
* on both {@linkplain LogicalSide logical sides}.
*/
public final class ModMismatchEvent implements IModBusEvent {
public static EventBus getBus(BusGroup modBusGroup) {
return IModBusEvent.getBus(modBusGroup, ModMismatchEvent.class);
}
/**
* The level being loaded. Useful for things like {@link net.minecraft.world.level.storage.DimensionDataStorage}
* to manage multiple files changing between mod versions.
*/
private final LevelStorageSource.LevelDirectory levelDirectory;
/**
* A set of previously-known versions that have mismatched with the currently loaded versions.
*/
private final HashMap versionDifferences;
/**
* Which mods have specified that they have handled version mismatches.
*/
private final HashMap resolved;
@ApiStatus.Internal
public ModMismatchEvent(LevelStorageSource.LevelDirectory levelDirectory, Map previousVersions, Map missingVersions) {
this.levelDirectory = levelDirectory;
this.resolved = new HashMap<>(previousVersions.size());
this.versionDifferences = new HashMap<>();
previousVersions.forEach((modId, version) -> versionDifferences.put(modId, new MismatchedVersionInfo(version, ModList.get()
.getModContainerById(modId)
.map(ModContainer::getModInfo)
.map(IModInfo::getVersion)
.orElse(null))));
missingVersions.forEach((modId, version) -> versionDifferences.put(modId, new MismatchedVersionInfo(version, null)));
}
/**
* Gets the current level directory for the world being loaded.
* Can be used for file operations and manual modification of mod files before world load.
*/
public LevelStorageSource.LevelDirectory getLevelDirectory() {
return this.levelDirectory;
}
/**
* Fetch a previous version of a given mod, if it has been mismatched.
* @param modId The mod to fetch previous version for.
* @return The previously known mod version, or {@link Optional#empty()} if unknown/not found.
*/
@Nullable
public ArtifactVersion getPreviousVersion(String modId) {
if (this.versionDifferences.containsKey(modId))
return this.versionDifferences.get(modId).oldVersion();
return null;
}
@Nullable
public ArtifactVersion getCurrentVersion(String modid) {
if (this.versionDifferences.containsKey(modid))
return this.versionDifferences.get(modid).newVersion();
return null;
}
/**
* Marks the mod version mismatch as having been resolved safely by the current mod.
*/
public void markResolved(String modId) {
final var resolvedBy = ModLoadingContext.get().getActiveContainer();
resolved.putIfAbsent(modId, resolvedBy);
}
/**
* Fetches the status of a mod mismatch handling state.
*/
public boolean wasResolved(String modId) {
return this.resolved.containsKey(modId);
}
public Optional getVersionDifference(String modid) {
return Optional.ofNullable(this.versionDifferences.get(modid));
}
public Optional getResolver(String modid) {
return Optional.ofNullable(this.resolved.get(modid));
}
public boolean anyUnresolved() {
return resolved.size() < versionDifferences.size();
}
public Stream getUnresolved() {
return versionDifferences.keySet().stream()
.filter(modid -> !resolved.containsKey(modid))
.map(unresolved -> new MismatchResolutionResult(unresolved, versionDifferences.get(unresolved), null))
.sorted(Comparator.comparing(MismatchResolutionResult::modid));
}
public boolean anyResolved() {
return !resolved.isEmpty();
}
public Stream getResolved() {
return resolved.keySet().stream()
.map(modid -> new MismatchResolutionResult(modid, versionDifferences.get(modid), resolved.get(modid)))
.sorted(Comparator.comparing(MismatchResolutionResult::modid));
}
public record MismatchResolutionResult(String modid, MismatchedVersionInfo versionDifference, @Nullable ModContainer resolver) {
public boolean wasSelfResolved() {
return resolver != null && resolver.getModId().equals(modid);
}
}
public record MismatchedVersionInfo(ArtifactVersion oldVersion, @Nullable ArtifactVersion newVersion) {
public boolean isMissing() {
return newVersion == null;
}
public boolean wasUpgrade() {
if (newVersion == null) return false;
return newVersion.compareTo(oldVersion) > 0;
}
}
}