Add plugin & component to commands, unregister on disable

#88
This commit is contained in:
Norbi Peti 2020-02-14 19:20:20 +01:00
parent d2ab3511c2
commit 23f3c0f133
No known key found for this signature in database
GPG key ID: DBA4C4549A927E56
9 changed files with 119 additions and 43 deletions

View file

@ -5,7 +5,6 @@ import buttondevteam.lib.architecture.ButtonPlugin;
import buttondevteam.lib.architecture.Component; import buttondevteam.lib.architecture.Component;
import buttondevteam.lib.chat.Command2; import buttondevteam.lib.chat.Command2;
import buttondevteam.lib.chat.Command2.Subcommand; import buttondevteam.lib.chat.Command2.Subcommand;
import buttondevteam.lib.chat.Command2MC;
import buttondevteam.lib.chat.CommandClass; import buttondevteam.lib.chat.CommandClass;
import buttondevteam.lib.chat.ICommand2MC; import buttondevteam.lib.chat.ICommand2MC;
import lombok.val; import lombok.val;
@ -19,9 +18,8 @@ import java.util.Optional;
"Component command", "Component command",
"Can be used to enable/disable/list components" "Can be used to enable/disable/list components"
}) })
public class ComponentCommand extends ICommand2MC<MainPlugin> { public class ComponentCommand extends ICommand2MC {
@Override public ComponentCommand() {
public void onRegister(Command2MC<MainPlugin> manager) {
getManager().addParamConverter(Plugin.class, arg -> Bukkit.getPluginManager().getPlugin(arg), "Plugin not found!"); getManager().addParamConverter(Plugin.class, arg -> Bukkit.getPluginManager().getPlugin(arg), "Plugin not found!");
} }
@ -31,7 +29,7 @@ public class ComponentCommand extends ICommand2MC<MainPlugin> {
}) })
public boolean enable(CommandSender sender, Plugin plugin, String component) { public boolean enable(CommandSender sender, Plugin plugin, String component) {
if (plugin instanceof ButtonPlugin) { if (plugin instanceof ButtonPlugin) {
if (!((ButtonPlugin<?>) plugin).justReload()) { if (!((ButtonPlugin) plugin).justReload()) {
sender.sendMessage("§cCouldn't reload config, check console."); sender.sendMessage("§cCouldn't reload config, check console.");
return true; return true;
} }

View file

@ -44,7 +44,7 @@ import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.logging.Logger; import java.util.logging.Logger;
public class MainPlugin extends ButtonPlugin<MainPlugin> { public class MainPlugin extends ButtonPlugin {
public static MainPlugin Instance; public static MainPlugin Instance;
public static Permission permission; public static Permission permission;
@Nullable @Nullable
@ -117,8 +117,8 @@ public class MainPlugin extends ButtonPlugin<MainPlugin> {
if (Bukkit.getPluginManager().isPluginEnabled("Votifier") && economy != null) if (Bukkit.getPluginManager().isPluginEnabled("Votifier") && economy != null)
Component.registerComponent(this, new VotifierComponent(economy)); Component.registerComponent(this, new VotifierComponent(economy));
ComponentManager.enableComponents(); ComponentManager.enableComponents();
getCommand2MC().registerCommand(new ComponentCommand()); registerCommand(new ComponentCommand());
getCommand2MC().registerCommand(new ChromaCommand()); registerCommand(new ChromaCommand());
TBMCCoreAPI.RegisterEventsForExceptions(new PlayerListener(), this); TBMCCoreAPI.RegisterEventsForExceptions(new PlayerListener(), this);
TBMCCoreAPI.RegisterEventsForExceptions(getCommand2MC(), this); TBMCCoreAPI.RegisterEventsForExceptions(getCommand2MC(), this);
ChromaGamerBase.addConverter(commandSender -> Optional.ofNullable(commandSender instanceof ConsoleCommandSender || commandSender instanceof BlockCommandSender ChromaGamerBase.addConverter(commandSender -> Optional.ofNullable(commandSender instanceof ConsoleCommandSender || commandSender instanceof BlockCommandSender

View file

@ -2,13 +2,13 @@ package buttondevteam.core;
import buttondevteam.core.component.channel.Channel; import buttondevteam.core.component.channel.Channel;
import buttondevteam.core.component.channel.ChannelComponent; import buttondevteam.core.component.channel.ChannelComponent;
import buttondevteam.lib.architecture.ButtonPlugin;
import buttondevteam.lib.architecture.Component; import buttondevteam.lib.architecture.Component;
import buttondevteam.lib.chat.Color; import buttondevteam.lib.chat.Color;
import buttondevteam.lib.chat.TBMCChatAPI; import buttondevteam.lib.chat.TBMCChatAPI;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.Server; import org.bukkit.Server;
import org.bukkit.plugin.PluginManager; import org.bukkit.plugin.PluginManager;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.scheduler.BukkitScheduler; import org.bukkit.scheduler.BukkitScheduler;
import org.mockito.Mockito; import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock; import org.mockito.invocation.InvocationOnMock;
@ -42,7 +42,7 @@ public class TestPrepare {
} }
})); }));
//noinspection unchecked //noinspection unchecked
Component.registerComponent(Mockito.mock(ButtonPlugin.class), new ChannelComponent()); Component.registerComponent(Mockito.mock(JavaPlugin.class), new ChannelComponent());
TBMCChatAPI.RegisterChatChannel(Channel.GlobalChat = new Channel("§fg§f", Color.White, "g", null)); TBMCChatAPI.RegisterChatChannel(Channel.GlobalChat = new Channel("§fg§f", Color.White, "g", null));
} }
} }

View file

@ -4,6 +4,7 @@ import buttondevteam.buttonproc.HasConfig;
import buttondevteam.core.ComponentManager; import buttondevteam.core.ComponentManager;
import buttondevteam.lib.TBMCCoreAPI; import buttondevteam.lib.TBMCCoreAPI;
import buttondevteam.lib.chat.Command2MC; import buttondevteam.lib.chat.Command2MC;
import buttondevteam.lib.chat.ICommand2MC;
import lombok.AccessLevel; import lombok.AccessLevel;
import lombok.Getter; import lombok.Getter;
import org.bukkit.configuration.InvalidConfigurationException; import org.bukkit.configuration.InvalidConfigurationException;
@ -22,9 +23,9 @@ import java.util.Optional;
import java.util.Stack; import java.util.Stack;
@HasConfig(global = true) @HasConfig(global = true)
public abstract class ButtonPlugin<TP extends ButtonPlugin<TP>> extends JavaPlugin { public abstract class ButtonPlugin extends JavaPlugin {
@Getter //Needs to be static as we don't know the plugin when a command is handled @Getter //Needs to be static as we don't know the plugin when a command is handled
private Command2MC<TP> command2MC = new Command2MC<>(); private static Command2MC command2MC = new Command2MC();
@Getter(AccessLevel.PROTECTED) @Getter(AccessLevel.PROTECTED)
private IHaveConfig iConfig; private IHaveConfig iConfig;
private CommentedConfiguration yaml; private CommentedConfiguration yaml;
@ -84,7 +85,7 @@ public abstract class ButtonPlugin<TP extends ButtonPlugin<TP>> extends JavaPlug
if (ConfigData.saveNow(getConfig())) if (ConfigData.saveNow(getConfig()))
getLogger().info("Saved configuration changes."); getLogger().info("Saved configuration changes.");
iConfig = null; //Clearing the hashmap is not enough, we need to update the section as well iConfig = null; //Clearing the hashmap is not enough, we need to update the section as well
//TBMCChatAPI.RemoveCommands(this); - TODO getCommand2MC().unregisterCommands(this);
} catch (Exception e) { } catch (Exception e) {
TBMCCoreAPI.SendException("Error while disabling plugin " + getName() + "!", e); TBMCCoreAPI.SendException("Error while disabling plugin " + getName() + "!", e);
} }
@ -147,6 +148,16 @@ public abstract class ButtonPlugin<TP extends ButtonPlugin<TP>> extends JavaPlug
} }
} }
/**
* Registers command and sets its plugin.
*
* @param command The command to register
*/
protected void registerCommand(ICommand2MC command) {
command.registerToPlugin(this);
getCommand2MC().registerCommand(command);
}
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE) @Target(ElementType.TYPE)
public @interface ConfigOpts { public @interface ConfigOpts {

View file

@ -23,8 +23,8 @@ import java.util.stream.Collectors;
* Configuration is based on class name * Configuration is based on class name
*/ */
@HasConfig(global = false) //Used for obtaining javadoc @HasConfig(global = false) //Used for obtaining javadoc
public abstract class Component<TP extends ButtonPlugin> { public abstract class Component<TP extends JavaPlugin> {
private static HashMap<Class<? extends Component>, Component<? extends JavaPlugin>> components = new HashMap<>(); @SuppressWarnings("rawtypes") private static HashMap<Class<? extends Component>, Component<? extends JavaPlugin>> components = new HashMap<>();
@Getter @Getter
private boolean enabled = false; private boolean enabled = false;
@ -44,11 +44,12 @@ public abstract class Component<TP extends ButtonPlugin> {
* Registers a component checking it's dependencies and calling {@link #register(JavaPlugin)}.<br> * Registers a component checking it's dependencies and calling {@link #register(JavaPlugin)}.<br>
* Make sure to register the dependencies first.<br> * Make sure to register the dependencies first.<br>
* The component will be enabled automatically, regardless of when it was registered.<br> * The component will be enabled automatically, regardless of when it was registered.<br>
* <b>If not using {@link ButtonPlugin}, call {@link ComponentManager#unregComponents(ButtonPlugin)} on plugin disable.</b>
* *
* @param component The component to register * @param component The component to register
* @return Whether the component is registered successfully (it may have failed to enable) * @return Whether the component is registered successfully (it may have failed to enable)
*/ */
public static <T extends ButtonPlugin> boolean registerComponent(T plugin, Component<T> component) { public static <T extends JavaPlugin> boolean registerComponent(T plugin, Component<T> component) {
return registerUnregisterComponent(plugin, component, true); return registerUnregisterComponent(plugin, component, true);
} }
@ -64,11 +65,11 @@ public abstract class Component<TP extends ButtonPlugin> {
return registerUnregisterComponent(plugin, component, false); return registerUnregisterComponent(plugin, component, false);
} }
public static <T extends ButtonPlugin> boolean registerUnregisterComponent(T plugin, Component<T> component, boolean register) { public static <T extends JavaPlugin> boolean registerUnregisterComponent(T plugin, Component<T> component, boolean register) {
try { try {
val metaAnn = component.getClass().getAnnotation(ComponentMetadata.class); val metaAnn = component.getClass().getAnnotation(ComponentMetadata.class);
if (metaAnn != null) { if (metaAnn != null) {
Class<? extends Component>[] dependencies = metaAnn.depends(); @SuppressWarnings("rawtypes") Class<? extends Component>[] dependencies = metaAnn.depends();
for (val dep : dependencies) { //TODO: Support dependencies at enable/disable as well for (val dep : dependencies) { //TODO: Support dependencies at enable/disable as well
if (!components.containsKey(dep)) { if (!components.containsKey(dep)) {
plugin.getLogger().warning("Failed to " + (register ? "" : "un") + "register component " + component.getClassName() + " as a required dependency is missing/disabled: " + dep.getSimpleName()); plugin.getLogger().warning("Failed to " + (register ? "" : "un") + "register component " + component.getClassName() + " as a required dependency is missing/disabled: " + dep.getSimpleName());
@ -85,7 +86,8 @@ public abstract class Component<TP extends ButtonPlugin> {
updateConfig(plugin, component); updateConfig(plugin, component);
component.register(plugin); component.register(plugin);
components.put(component.getClass(), component); components.put(component.getClass(), component);
plugin.getComponentStack().push(component); if (plugin instanceof ButtonPlugin)
((ButtonPlugin) plugin).getComponentStack().push(component);
if (ComponentManager.areComponentsEnabled() && component.shouldBeEnabled().get()) { if (ComponentManager.areComponentsEnabled() && component.shouldBeEnabled().get()) {
try { //Enable components registered after the previous ones getting enabled try { //Enable components registered after the previous ones getting enabled
setComponentEnabled(component, true); setComponentEnabled(component, true);
@ -138,8 +140,7 @@ public abstract class Component<TP extends ButtonPlugin> {
//System.out.println("Done enabling "+component.getClassName()); //System.out.println("Done enabling "+component.getClassName());
} else { } else {
component.disable(); component.disable();
//TBMCChatAPI.RemoveCommands(component); - TODO ButtonPlugin.getCommand2MC().unregisterCommands(component);
component.getPlugin().getCommand2MC().unregisterCommand();
} }
} }
@ -160,6 +161,7 @@ public abstract class Component<TP extends ButtonPlugin> {
* *
* @return The currently registered components * @return The currently registered components
*/ */
@SuppressWarnings("rawtypes")
public static Map<Class<? extends Component>, Component<? extends JavaPlugin>> getComponents() { public static Map<Class<? extends Component>, Component<? extends JavaPlugin>> getComponents() {
return Collections.unmodifiableMap(components); return Collections.unmodifiableMap(components);
} }
@ -200,13 +202,16 @@ public abstract class Component<TP extends ButtonPlugin> {
protected abstract void disable(); protected abstract void disable();
/** /**
* Registers a TBMCCommand to the component. Make sure to use {@link buttondevteam.lib.chat.CommandClass} and {@link buttondevteam.lib.chat.Command2.Subcommand}. * Registers a command to the component. Make sure to use {@link buttondevteam.lib.chat.CommandClass} and {@link buttondevteam.lib.chat.Command2.Subcommand}.
* You don't need to register the command in plugin.yml. * You don't need to register the command in plugin.yml.
* *
* @param commandBase Custom coded command class * @param command Custom coded command class
*/ */
protected final <T extends ButtonPlugin<T>> void registerCommand(ICommand2MC<T> commandBase) { protected final void registerCommand(ICommand2MC command) {
getPlugin().getCommand2MC().registerCommand(commandBase); if (plugin instanceof ButtonPlugin)
command.registerToPlugin((ButtonPlugin) plugin);
command.registerToComponent(this);
ButtonPlugin.getCommand2MC().registerCommand(command);
} }
/** /**

View file

@ -114,6 +114,8 @@ public abstract class Command2<TC extends ICommand2, TP extends Command2Sender>
private ArrayList<String> commandHelp = new ArrayList<>(); //Mainly needed by Discord private ArrayList<String> commandHelp = new ArrayList<>(); //Mainly needed by Discord
private char commandChar;
/** /**
* Adds a param converter that obtains a specific object from a string parameter. * Adds a param converter that obtains a specific object from a string parameter.
* The converter may return null. * The converter may return null.
@ -260,6 +262,7 @@ public abstract class Command2<TC extends ICommand2, TP extends Command2Sender>
public abstract void registerCommand(TC command); public abstract void registerCommand(TC command);
protected void registerCommand(TC command, @SuppressWarnings("SameParameterValue") char commandChar) { protected void registerCommand(TC command, @SuppressWarnings("SameParameterValue") char commandChar) {
this.commandChar = commandChar;
val path = command.getCommandPath(); val path = command.getCommandPath();
int x = path.indexOf(' '); int x = path.indexOf(' ');
val mainPath = commandChar + path.substring(0, x == -1 ? path.length() : x); val mainPath = commandChar + path.substring(0, x == -1 ? path.length() : x);
@ -357,8 +360,19 @@ public abstract class Command2<TC extends ICommand2, TP extends Command2Sender>
return Collections.unmodifiableSet(subcommands.keySet()); return Collections.unmodifiableSet(subcommands.keySet());
}*/ }*/
public void unregisterCommand() { /**
* Unregisters all of the subcommands in the given command.
*
* @param command The command object
*/
public void unregisterCommand(ICommand2<TP> command) {
var path = command.getCommandPath();
for (val method : command.getClass().getMethods()) {
val ann = method.getAnnotation(Subcommand.class);
if (ann == null) continue;
val subcommand = commandChar + path + getCommandPath(method.getName(), ' ');
subcommands.remove(subcommand);
}
} }
/** /**

View file

@ -2,6 +2,7 @@ package buttondevteam.lib.chat;
import buttondevteam.core.MainPlugin; import buttondevteam.core.MainPlugin;
import buttondevteam.lib.architecture.ButtonPlugin; import buttondevteam.lib.architecture.ButtonPlugin;
import buttondevteam.lib.architecture.Component;
import lombok.val; import lombok.val;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer; import org.bukkit.OfflinePlayer;
@ -18,11 +19,17 @@ import java.lang.annotation.Annotation;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.Optional;
import java.util.function.Function; import java.util.function.Function;
public class Command2MC<TP extends ButtonPlugin<TP>> extends Command2<ICommand2MC<TP>, Command2MCSender> implements Listener { public class Command2MC extends Command2<ICommand2MC, Command2MCSender> implements Listener {
/**
* Don't use directly, use the method in Component and ButtonPlugin to automatically unregister the command when needed.
*
* @param command The command to register
*/
@Override @Override
public void registerCommand(ICommand2MC<TP> command) { public void registerCommand(ICommand2MC command) {
super.registerCommand(command, '/'); super.registerCommand(command, '/');
var perm = "chroma.command." + command.getCommandPath().replace(' ', '.'); var perm = "chroma.command." + command.getCommandPath().replace(' ', '.');
if (Bukkit.getPluginManager().getPermission(perm) == null) //Check needed for plugin reset if (Bukkit.getPluginManager().getPermission(perm) == null) //Check needed for plugin reset
@ -47,11 +54,11 @@ public class Command2MC<TP extends ButtonPlugin<TP>> extends Command2<ICommand2M
} }
@Override @Override
public boolean hasPermission(Command2MCSender sender, ICommand2MC<TP> command, Method method) { public boolean hasPermission(Command2MCSender sender, ICommand2MC command, Method method) {
return hasPermission(sender.getSender(), command, method); return hasPermission(sender.getSender(), command, method);
} }
public boolean hasPermission(CommandSender sender, ICommand2MC<TP> command, Method method) { public boolean hasPermission(CommandSender sender, ICommand2MC command, Method method) {
if (sender instanceof ConsoleCommandSender) return true; //Always allow the console if (sender instanceof ConsoleCommandSender) return true; //Always allow the console
String pg; String pg;
boolean p = true; boolean p = true;
@ -81,7 +88,7 @@ public class Command2MC<TP extends ButtonPlugin<TP>> extends Command2<ICommand2M
* @param method The subcommand to check * @param method The subcommand to check
* @return The permission group for the subcommand or empty string * @return The permission group for the subcommand or empty string
*/ */
private String permGroup(ICommand2MC<TP> command, Method method) { private String permGroup(ICommand2MC command, Method method) {
val sc = method.getAnnotation(Subcommand.class); val sc = method.getAnnotation(Subcommand.class);
if (sc != null && sc.permGroup().length() > 0) { if (sc != null && sc.permGroup().length() > 0) {
return sc.permGroup(); return sc.permGroup();
@ -120,6 +127,21 @@ public class Command2MC<TP extends ButtonPlugin<TP>> extends Command2<ICommand2M
super.addParamConverter(cl, converter, "§c" + errormsg); super.addParamConverter(cl, converter, "§c" + errormsg);
} }
public void unregisterCommands(ButtonPlugin plugin) {
/*var cmds = subcommands.values().stream().map(sd -> sd.command).filter(cmd -> plugin.equals(cmd.getPlugin())).toArray(ICommand2MC[]::new);
for (var cmd : cmds)
unregisterCommand(cmd);*/
subcommands.values().removeIf(sd -> plugin.equals(sd.command.getPlugin()));
}
public void unregisterCommands(Component<?> component) {
/*var cmds = subcommands.values().stream().map(sd -> sd.command).filter(cmd -> component.equals(cmd.getComponent())).toArray(ICommand2MC[]::new);
for (var cmd : cmds)
unregisterCommand(cmd);*/
subcommands.values().removeIf(sd -> Optional.ofNullable(sd.command.getComponent())
.map(comp -> component.getClass().getSimpleName().equals(comp.getClass().getSimpleName())).orElse(false));
}
@EventHandler @EventHandler
private void handleTabComplete(TabCompleteEvent event) { private void handleTabComplete(TabCompleteEvent event) {
String commandline = event.getBuffer(); String commandline = event.getBuffer();
@ -129,7 +151,7 @@ public class Command2MC<TP extends ButtonPlugin<TP>> extends Command2<ICommand2M
String subcommand = commandline.substring(0, i).toLowerCase(); String subcommand = commandline.substring(0, i).toLowerCase();
if (subcommand.length() == 0 || subcommand.charAt(0) != '/') subcommand = '/' + subcommand; //Console if (subcommand.length() == 0 || subcommand.charAt(0) != '/') subcommand = '/' + subcommand; //Console
//System.out.println("Subcommand: " + subcommand); //System.out.println("Subcommand: " + subcommand);
SubcommandData<ICommand2MC<TP>> sd = subcommands.get(subcommand); //O(1) SubcommandData<ICommand2MC> sd = subcommands.get(subcommand); //O(1)
if (sd == null) continue; if (sd == null) continue;
//System.out.println("ht: " + Arrays.toString(sd.helpText)); //System.out.println("ht: " + Arrays.toString(sd.helpText));
Arrays.stream(sd.helpText).skip(1).map(ht -> new HashMap.SimpleEntry<>(ht, subcommands.get(ht))).filter(e -> e.getValue() != null) Arrays.stream(sd.helpText).skip(1).map(ht -> new HashMap.SimpleEntry<>(ht, subcommands.get(ht))).filter(e -> e.getValue() != null)

View file

@ -45,13 +45,10 @@ public abstract class ICommand2<TP extends Command2Sender> {
private final String path; private final String path;
@Getter @Getter
private Command2<?, TP> manager; //TIL that if I use a raw type on a variable then none of the type args will work (including what's defined on a method, not on the type) private final Command2<?, TP> manager; //TIL that if I use a raw type on a variable then none of the type args will work (including what's defined on a method, not on the type)
public <T extends ICommand2<TP>> ICommand2() { public <T extends ICommand2<TP>> ICommand2(Command2<T, TP> manager) {
path = getcmdpath(); path = getcmdpath();
}
public <T extends ICommand2<TP>> void onRegister(Command2<T, TP> manager) {
this.manager = manager; this.manager = manager;
} }

View file

@ -1,17 +1,46 @@
package buttondevteam.lib.chat; package buttondevteam.lib.chat;
import buttondevteam.lib.architecture.ButtonPlugin; import buttondevteam.lib.architecture.ButtonPlugin;
import buttondevteam.lib.architecture.Component;
import lombok.Getter;
import javax.annotation.Nullable;
@SuppressWarnings("JavadocReference")
public abstract class ICommand2MC extends ICommand2<Command2MCSender> {
@Getter
private ButtonPlugin plugin;
@Getter
@Nullable
private Component<?> component;
public abstract class ICommand2MC<T extends ButtonPlugin<T>> extends ICommand2<Command2MCSender> {
public ICommand2MC() { public ICommand2MC() {
super(ButtonPlugin.getCommand2MC());
} }
public void onRegister(Command2MC<T> manager) { /**
* Called from {@link buttondevteam.lib.architecture.Component#registerCommand(ICommand2MC)} and {@link ButtonPlugin#registerCommand(ICommand2MC)}
*/
public void registerToPlugin(ButtonPlugin plugin) {
if (this.plugin == null)
this.plugin = plugin;
else
throw new IllegalStateException("The command is already assigned to a plugin!");
} }
@Override /**
* Called from {@link buttondevteam.lib.architecture.Component#registerCommand(ICommand2MC)}
*/
public void registerToComponent(Component<?> component) {
if (this.component == null)
this.component = component;
else
throw new IllegalStateException("The command is already assigned to a component!");
}
/*@Override
public <TX extends ICommand2<Command2MCSender>> void onRegister(Command2<TX, Command2MCSender> manager) { public <TX extends ICommand2<Command2MCSender>> void onRegister(Command2<TX, Command2MCSender> manager) {
super.onRegister(manager); super.onRegister(manager);
onRegister((Command2MC<T>) manager); onRegister((Command2MC) manager); //If ICommand2 is inherited with the same type arg, this would fail but I don't want to add another type param to ICommand2
} } //For example: class IOffender extends ICommand2<Command2MCSender>*/
} }