/* * This file is part of Sponge, licensed under the MIT License (MIT). * * Copyright (c) SpongePowered <https://www.spongepowered.org> * Copyright (c) contributors * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.spongepowered.mod.plugin; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Strings.isNullOrEmpty; import static org.spongepowered.api.plugin.Plugin.ID_PATTERN; import com.google.common.base.Objects; import com.google.common.collect.ImmutableList; import com.google.common.eventbus.EventBus; import com.google.common.eventbus.Subscribe; import com.google.inject.Inject; import com.google.inject.Injector; import net.minecraftforge.fml.common.FMLCommonHandler; import net.minecraftforge.fml.common.ILanguageAdapter; import net.minecraftforge.fml.common.LoadController; import net.minecraftforge.fml.common.Loader; import net.minecraftforge.fml.common.MetadataCollection; import net.minecraftforge.fml.common.ModClassLoader; import net.minecraftforge.fml.common.ModContainer; import net.minecraftforge.fml.common.ModMetadata; import net.minecraftforge.fml.common.ProxyInjector; import net.minecraftforge.fml.common.discovery.ModCandidate; import net.minecraftforge.fml.common.event.FMLConstructionEvent; import net.minecraftforge.fml.common.versioning.ArtifactVersion; import net.minecraftforge.fml.common.versioning.DefaultArtifactVersion; import net.minecraftforge.fml.common.versioning.VersionParser; import net.minecraftforge.fml.common.versioning.VersionRange; import org.spongepowered.api.Sponge; import org.spongepowered.api.plugin.PluginContainer; import org.spongepowered.common.SpongeImpl; import org.spongepowered.common.inject.plugin.PluginModule; import org.spongepowered.common.plugin.PluginContainerExtension; import java.io.File; import java.net.URL; import java.security.cert.Certificate; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; // PluginContainer is implemented indirectly through the mixin to ModContainer public class SpongeModPluginContainer implements ModContainer, PluginContainerExtension { // This is the implementation (SpongeForge) injector. @Inject private static Injector spongeInjector; private final String id; private final String className; private final ModCandidate candidate; private final Map<String, Object> descriptor; private ModMetadata metadata; private boolean invalid; // Cannot throw until plugin is getting constructed private boolean enabled = true; private Object instance; private DefaultArtifactVersion processedVersion; private LoadController controller; private Injector injector; private PluginContainer pluginContainer = (PluginContainer) (Object) this; private static final String ID_WARNING = "Plugin IDs should be lowercase, and only contain characters from " + "a-z, dashes or underscores, start with a lowercase letter, and not exceed 64 characters."; public SpongeModPluginContainer(String className, ModCandidate candidate, Map<String, Object> descriptor) { this.id = checkNotNull((String) descriptor.get("id"), "id"); this.className = className; this.candidate = candidate; this.descriptor = descriptor; if (!ID_PATTERN.matcher(this.id).matches()) { SpongeImpl.getLogger().error("Skipping plugin with invalid plugin ID '{}'. " + ID_WARNING, this.id); this.invalid = true; } } @Override public String getModId() { return this.id; } @Override public String getName() { return this.metadata.name; } @Override public String getVersion() { return this.metadata.version; } @Override public File getSource() { return this.candidate.getModContainer(); } @Override public ModMetadata getMetadata() { return this.metadata; } @Override @SuppressWarnings("unchecked") public void bindMetadata(MetadataCollection mc) { this.metadata = mc.getMetadataForId(this.id, this.descriptor); if (isNullOrEmpty(this.metadata.name)) { this.metadata.name = this.id; } if (this.metadata.version == null) { this.metadata.version = ""; } if (this.metadata.autogenerated) { if (!this.invalid) { SpongeImpl.getLogger().warn("Plugin '{}' seems to be missing a valid mcmod.info metadata file. This is not a problem when testing " + "plugins, however it is recommended to include one in public plugins.\n" + "Please see https://docs.spongepowered.org/master/en/plugin/plugin-meta.html for details.", this.id); } // Version is set in the dummy automatically (see getMetadataForId) this.metadata.description = getDescriptorValue("description"); this.metadata.url = getDescriptorValue("url"); Collection<String> authors = (Collection<String>) this.descriptor.get("authors"); if (authors != null) { this.metadata.authorList = new ArrayList<>(authors); } Object deps = this.descriptor.get("dependencies"); if (deps != null) { Iterable<Map<String, Object>> depDescriptors = (Iterable<Map<String, Object>>) this.descriptor.get("dependencies"); if (depDescriptors != null) { Set<ArtifactVersion> requirements = this.metadata.requiredMods; List<ArtifactVersion> dependencies = this.metadata.dependencies; for (Map<String, Object> depDescriptor : depDescriptors) { String dep = checkNotNull((String) depDescriptor.get("id"), "dependency id"); if (this.id.equals(dep)) { this.invalid = true; SpongeImpl.getLogger().error("Plugin '{}' cannot have a dependency on itself. This is redundant and should be " + "removed.", this.id); continue; } String depVersion = (String) depDescriptor.get("version"); ArtifactVersion dependency; if (isNullOrEmpty(depVersion)) { dependency = new DefaultArtifactVersion(dep, true); } else { dependency = new DefaultArtifactVersion(dep, VersionParser.parseRange(depVersion)); } Boolean optional = (Boolean) depDescriptor.get("optional"); if (optional == null || !optional) { requirements.add(dependency); } // TODO: Load order dependencies.add(dependency); } } } } else { // Check dependencies Iterator<ArtifactVersion> itr = this.metadata.requiredMods.iterator(); while (itr.hasNext()) { if (this.id.equals(itr.next().getLabel())) { SpongeImpl.getLogger().warn("Plugin '{}' requires itself to be loaded. This is redundant and can be removed from the " + "dependencies.", this.id); itr.remove(); } } if (!this.metadata.dependants.isEmpty()) { SpongeImpl.getLogger().error("Invalid dependency with load order AFTER on plugin '{}'. This is currently not supported for Sponge " + "plugins. Requested dependencies: {}", this.id, this.metadata.dependants); this.invalid = true; } this.metadata.dependants = ImmutableList.of(); } } private String getDescriptorValue(String key) { return (String) this.descriptor.getOrDefault(key, ""); } @Override public void setEnabledState(boolean isEnabled) { this.enabled = isEnabled; } @Override public Set<ArtifactVersion> getRequirements() { return this.metadata.requiredMods; } @Override public List<ArtifactVersion> getDependencies() { return this.metadata.dependencies; } @Override public List<ArtifactVersion> getDependants() { return this.metadata.dependants; } @Override public String getSortingRules() { return this.metadata.printableSortingRules(); } @Override public boolean matches(Object mod) { return this.instance == mod; } @Override public Object getMod() { return this.instance; } @Override public boolean registerBus(EventBus bus, LoadController controller) { if (this.enabled) { this.controller = controller; bus.register(this); return true; } return false; } @Subscribe public void constructMod(FMLConstructionEvent event) { try { if (this.invalid) { throw new InvalidPluginException(); } // Add source file to classloader so we can load it. ModClassLoader modClassLoader = event.getModClassLoader(); modClassLoader.addFile(getSource()); modClassLoader.clearNegativeCacheFor(this.candidate.getClassList()); Class<?> pluginClazz = Class.forName(this.className, true, modClassLoader); Injector injector = spongeInjector.getParent().createChildInjector(new PluginModule((PluginContainer) this, pluginClazz)); this.injector = injector; this.instance = injector.getInstance(pluginClazz); // TODO: Detect Scala or use meta to know if we're scala and use proper adapter here... ProxyInjector.inject(this, event.getASMHarvestedData(), FMLCommonHandler.instance().getSide(), new ILanguageAdapter.JavaAdapter()); Sponge.getEventManager().registerListeners(this, this.instance); } catch (Throwable t) { this.controller.errorOccurred(this, t); } } @Override public ArtifactVersion getProcessedVersion() { if (this.processedVersion == null) { String version = getVersion(); if (isNullOrEmpty(version)) { this.processedVersion = new DefaultArtifactVersion(this.id, true); } else { this.processedVersion = new DefaultArtifactVersion(this.id, version); } } return this.processedVersion; } @Override public boolean isImmutable() { return false; } @Override public String getDisplayVersion() { return getVersion(); } @Override public VersionRange acceptableMinecraftVersionRange() { return Loader.instance().getMinecraftModContainer().getStaticVersionRange(); } @Override public Certificate getSigningCertificate() { return null; } @Override public Map<String, String> getCustomModProperties() { return EMPTY_PROPERTIES; } @Override public Class<?> getCustomResourcePackClass() { try { return Class.forName(getSource().isDirectory() ? "net.minecraftforge.fml.client.FMLFolderResourcePack" : "net.minecraftforge.fml.client.FMLFileResourcePack", true, getClass().getClassLoader()); } catch (ClassNotFoundException e) { return null; } } @Override public Map<String, String> getSharedModDescriptor() { Map<String, String> descriptor = new HashMap<>(); descriptor.put("modsystem", "Sponge"); descriptor.put("id", this.id); descriptor.put("version", getDisplayVersion()); descriptor.put("name", getName()); descriptor.put("url", this.metadata.url); descriptor.put("authors", this.metadata.getAuthorList()); descriptor.put("description", this.metadata.description); return descriptor; } @Override public Disableable canBeDisabled() { // Note: No flag to indicate if a plugin allows it, can only happen while server is not running. // (e.g., main menu in SSP). Defaulting to on restart only. return Disableable.RESTART; } @Override public String getGuiClassName() { // Note: Not needed, client-side only return null; } @Override public List<String> getOwnedPackages() { return this.candidate.getContainedPackages(); } @Override public boolean shouldLoadInEnvironment() { return true; } @Override public URL getUpdateUrl() { return null; } @Override public void setClassVersion(int classVersion) { } @Override public int getClassVersion() { return 0; } @Override public Injector getInjector() { return this.injector; } @Override public final String toString() { return Objects.toStringHelper("Plugin") .omitNullValues() .add("id", this.pluginContainer.getId()) .add("name", this.pluginContainer.getName()) .add("version", this.pluginContainer.getVersion().orElse(null)) .add("description", this.pluginContainer.getDescription().orElse(null)) .add("url", this.pluginContainer.getUrl().orElse(null)) .add("authors", this.pluginContainer.getAuthors().isEmpty() ? null : this.pluginContainer.getAuthors()) .add("source", this.pluginContainer.getSource().orElse(null)) .toString(); } }