From 9a920d95eebc17d58da03ad1ae20585dc5a41edd Mon Sep 17 00:00:00 2001 From: Major- Date: Fri, 12 Feb 2016 16:29:57 +0000 Subject: [PATCH] Add support for Events defined in Ruby A slightly complicated and hacky change, because this requires working around JRuby issue 2359. --- data/plugins/bootstrap.rb | 56 +++++++++++++++---- data/plugins/navigation/door/door.rb | 30 ++++++++-- .../src/main/org/apollo/game/model/World.java | 24 ++++---- .../apollo/game/model/event/ProxyEvent.java | 52 +++++++++++++++++ .../game/model/event/ProxyEventListener.java | 52 +++++++++++++++++ .../game/plugin/RubyPluginEnvironment.java | 10 +--- 6 files changed, 189 insertions(+), 35 deletions(-) create mode 100644 game/src/main/org/apollo/game/model/event/ProxyEvent.java create mode 100644 game/src/main/org/apollo/game/model/event/ProxyEventListener.java diff --git a/data/plugins/bootstrap.rb b/data/plugins/bootstrap.rb index 75c90ef9..f36c1493 100644 --- a/data/plugins/bootstrap.rb +++ b/data/plugins/bootstrap.rb @@ -18,6 +18,9 @@ java_import 'org.apollo.game.model.World' java_import 'org.apollo.game.model.entity.Player' java_import 'org.apollo.game.model.event.EventListener' java_import 'org.apollo.game.model.event.PlayerEvent' +java_import 'org.apollo.game.model.event.impl.LoginEvent' +java_import 'org.apollo.game.model.event.ProxyEvent' +java_import 'org.apollo.game.model.event.ProxyEventListener' java_import 'org.apollo.game.model.entity.setting.PrivilegeLevel' java_import 'org.apollo.game.scheduling.ScheduledTask' java_import 'org.apollo.game.plugin.PluginContext' @@ -129,23 +132,26 @@ def schedule(*args, &block) end end -# Defines some sort of action to take upon an message. The following types of -# message are currently valid: +@@proxy_listener = ProxyEventListener.new +$world.listen_for(ProxyEvent.java_class, @@proxy_listener) + +# Defines some sort of action to take upon an message. The following types of message are currently +# valid: # # * :command # * :message # * :button # * Any valid Event, as a symbol in ruby snake_case form. # -# A command takes one or two arguments (the command name and optionally the -# minimum rights level to use it). The minimum rights level defaults to -# STANDARD. The block should have two arguments: player and command. +# A command takes one or two arguments (the command name and optionally the minimum rights level to +# use it). The minimum rights level defaults to STANDARD. The block should have two arguments: +# player and command. # -# An message takes no arguments. The block should have three arguments: the chain -# context, the player and the message object. +# An message takes no arguments. The block should have two arguments: the player and the message +# object. # -# A button takes one argument (the id). The block should have one argument: the -# player who clicked the button. +# A button takes one argument (the id). The block should have one argument: the player who clicked +# the button. def on(type, *args, &block) case type when :command then on_command(args, block) @@ -153,11 +159,41 @@ def on(type, *args, &block) when :button then on_button(args, block) else class_name = type.to_s.camelize.concat('Event') - type = Java::JavaClass.for_name("org.apollo.game.model.event.impl.#{class_name}") + + begin + type = Java::JavaClass.for_name("org.apollo.game.model.event.impl.#{class_name}") + rescue + @@proxy_listener.add(class_name, ProcEventListener.new(block)) + return + end + $world.listen_for(type, ProcEventListener.new(block)) end end +# Contains extension methods for World. +module WorldExtensions + + # Overrides World#submit, providing special-case behaviour for Events defined in Ruby, which + # need to be wrapped in a ProxyEvent, until https://github.com/jruby/jruby/issues/2359 is + # resolved. + def submit(event) + if event.java_class.name.end_with?(".Event", ".PlayerEvent") + event = ProxyEvent.new(event.class.name, event) + end + + super(event) + end + +end + +# Prepend the methods defined in WorldExtensions to World. +class World + prepend WorldExtensions +end + +private + # Defines an action to be taken upon a button press. def on_button(args, proc) fail 'Button must have one argument.' unless args.length == 1 diff --git a/data/plugins/navigation/door/door.rb b/data/plugins/navigation/door/door.rb index 4c7e7797..b3adf58f 100644 --- a/data/plugins/navigation/door/door.rb +++ b/data/plugins/navigation/door/door.rb @@ -1,5 +1,8 @@ java_import 'org.apollo.game.action.DistancedAction' +java_import 'org.apollo.game.model.event.Event' + +private # A distanced action which opens a door. class OpenDoorAction < DistancedAction @@ -24,10 +27,29 @@ class OpenDoorAction < DistancedAction end -# MessageListener for opening and closing doors. +# A PlayerEvent that is fired when a player attempts to open a door. +class OpenDoorEvent < PlayerEvent + attr_reader :door + + def initialize(player, door) + super(player) + @door = door + end + +end + + +# Listens for FirstObjectActions performed on doors. on :message, :first_object_action do |player, message| - if DoorUtil.door?(message.id) - door = DoorUtil.get_door_object(message.position, message.id) - player.start_action(OpenDoorAction.new(player, door)) unless door.nil? + id = message.id + + if DoorUtil.door?(id) + position = message.position + door = DoorUtil.get_door_object(position, id) + + if !door.nil? && $world.submit(OpenDoorEvent.new(player, door)) + player.start_action(OpenDoorAction.new(player, door)) + end end end + diff --git a/game/src/main/org/apollo/game/model/World.java b/game/src/main/org/apollo/game/model/World.java index 27536ab8..09ca1962 100644 --- a/game/src/main/org/apollo/game/model/World.java +++ b/game/src/main/org/apollo/game/model/World.java @@ -7,6 +7,7 @@ import java.util.Map; import java.util.Queue; import java.util.logging.Logger; +import com.google.common.base.Preconditions; import org.apollo.Service; import org.apollo.cache.IndexedFileSystem; import org.apollo.cache.decoder.ItemDefinitionDecoder; @@ -31,8 +32,6 @@ import org.apollo.game.scheduling.Scheduler; import org.apollo.game.scheduling.impl.NpcMovementTask; import org.apollo.util.NameUtil; -import com.google.common.base.Preconditions; - /** * The world class is a singleton which contains objects like the {@link MobRepository} for players and NPCs. It should * only contain things relevant to the in-game world and not classes which deal with I/O and such (these may be better @@ -86,6 +85,11 @@ public final class World { */ private final MobRepository npcRepository = new MobRepository<>(WorldConstants.MAXIMUM_NPCS); + /** + * The Queue of Npcs that have yet to be removed from the repository. + */ + private final Queue oldNpcs = new ArrayDeque<>(); + /** * The {@link MobRepository} of {@link Player}s. */ @@ -100,11 +104,6 @@ public final class World { * The Queue of Npcs that have yet to be added to the repository. */ private final Queue queuedNpcs = new ArrayDeque<>(); - - /** - * The Queue of Npcs that have yet to be removed from the repository. - */ - private final Queue oldNpcs = new ArrayDeque<>(); /** * This world's {@link RegionRepository}. @@ -209,8 +208,8 @@ public final class World { releaseNumber = release; SynchronousDecoder decoder = new SynchronousDecoder(new ItemDefinitionDecoder(fs), - new NpcDefinitionDecoder(fs), new GameObjectDecoder(fs, this), - EquipmentDefinitionParser.fromFile("data/equipment-" + release + "" + ".dat")); + new NpcDefinitionDecoder(fs), new GameObjectDecoder(fs, this), + EquipmentDefinitionParser.fromFile("data/equipment-" + release + "" + ".dat")); decoder.block(); @@ -315,8 +314,7 @@ public final class World { * @param npc The npc. */ public void unregister(final Npc npc) { - Preconditions.checkNotNull(npc, "Npc may not be null."); - + Preconditions.checkNotNull(npc, "Npc must not be null."); oldNpcs.add(npc); } @@ -362,7 +360,7 @@ public final class World { npcMovement.addNpc(npc); } } else { - logger.warning("Failed to register npc, repository capacity reached: [count=" + npcRepository.size() + "]"); + logger.warning("Failed to register npc (capacity reached): [count=" + npcRepository.size() + "]"); } } } @@ -373,7 +371,7 @@ public final class World { private void unregisterNpcs() { while (!oldNpcs.isEmpty()) { Npc npc = oldNpcs.poll(); - + Region region = regions.fromPosition(npc.getPosition()); region.removeEntity(npc); diff --git a/game/src/main/org/apollo/game/model/event/ProxyEvent.java b/game/src/main/org/apollo/game/model/event/ProxyEvent.java new file mode 100644 index 00000000..b59c3dca --- /dev/null +++ b/game/src/main/org/apollo/game/model/event/ProxyEvent.java @@ -0,0 +1,52 @@ +package org.apollo.game.model.event; + +/** + * An {@link Event} that wraps another {@link Event}. + * + * This is a workaround for JRuby issue 2359. This class + * should not be used in Java. + * + * @author Major + */ +public final class ProxyEvent extends Event { + + /** + * The Event created by a Ruby plugin. + */ + private final Event event; + + /** + * The name of the Ruby Event. + */ + private final String name; + + /** + * Creates the ProxyEvent. + * + * @param name The name of the {@link Event} defined in Ruby. + * @param event The Event created by a Ruby plugin. + */ + public ProxyEvent(String name, Event event) { + this.name = name; + this.event = event; + } + + /** + * Gets the name of the {@link Event} defined in Ruby. + * + * @return The name. + */ + public String getName() { + return name; + } + + /** + * Gets the {@link Event} created in a Ruby plugin. + * + * @return The Ruby {@link Event}. + */ + public Event getRuby() { + return event; + } + +} diff --git a/game/src/main/org/apollo/game/model/event/ProxyEventListener.java b/game/src/main/org/apollo/game/model/event/ProxyEventListener.java new file mode 100644 index 00000000..4c973ef4 --- /dev/null +++ b/game/src/main/org/apollo/game/model/event/ProxyEventListener.java @@ -0,0 +1,52 @@ +package org.apollo.game.model.event; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * An {@link EventListener} for {@link ProxyEvent}s. + * + * This is a workaround for JRuby issue 2359. + * + * @author Major + */ +public final class ProxyEventListener implements EventListener { + + /** + * The {@link Map} from Event names to {@link List}s of {@link EventListener}s. + */ + private final Map>> listeners = new HashMap<>(); + + /** + * Registers an {@link EventListener} to this proxy. This method is not type-safe, and must only + * be called from ruby. + * + * @param name The name of the Event. Must not be {@code null}. + * @param listener The {@link EventListener} to add. Must not be {@code null}. + */ + public void add(String name, EventListener listener) { + List> listeners = this.listeners.computeIfAbsent(name, n -> new ArrayList<>(2)); + listeners.add(listener); + } + + @Override + @SuppressWarnings("unchecked") + public void handle(ProxyEvent event) { + List> chain = listeners.get(event.getName()); + + if (chain != null) { + for (EventListener listener : chain) { + Event ruby = event.getRuby(); + listener.handle(ruby); + + if (ruby.terminated()) { + event.terminate(); + break; + } + } + } + } + +} diff --git a/game/src/main/org/apollo/game/plugin/RubyPluginEnvironment.java b/game/src/main/org/apollo/game/plugin/RubyPluginEnvironment.java index 3ec06194..9ac48c67 100644 --- a/game/src/main/org/apollo/game/plugin/RubyPluginEnvironment.java +++ b/game/src/main/org/apollo/game/plugin/RubyPluginEnvironment.java @@ -20,11 +20,6 @@ public final class RubyPluginEnvironment implements PluginEnvironment { */ private final ScriptingContainer container = new ScriptingContainer(); - /** - * The World this RubyPluginEnvironment is for. - */ - private final World world; - /** * Creates and bootstraps the Ruby plugin environment. * @@ -32,7 +27,7 @@ public final class RubyPluginEnvironment implements PluginEnvironment { * @throws IOException If an I/O error occurs during bootstrapping. */ public RubyPluginEnvironment(World world) throws IOException { - this.world = world; + container.put("$world", world); parseBootstrapper(); } @@ -42,7 +37,7 @@ public final class RubyPluginEnvironment implements PluginEnvironment { container.runScriptlet(is, name); } catch (Exception e) { e.printStackTrace(); - throw new RuntimeException("Error parsing scriptlet " + name + "."); + throw new RuntimeException("Error parsing scriptlet " + name + ".", e); } } @@ -61,7 +56,6 @@ public final class RubyPluginEnvironment implements PluginEnvironment { @Override public void setContext(PluginContext context) { container.put("$ctx", context); - container.put("$world", world); } } \ No newline at end of file