Add support for Events defined in Ruby

A slightly complicated and hacky change, because this requires working
around JRuby issue 2359.
This commit is contained in:
Major-
2016-02-12 16:29:57 +00:00
parent 8e52dfb121
commit 9a920d95ee
6 changed files with 189 additions and 35 deletions
+46 -10
View File
@@ -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
+26 -4
View File
@@ -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
+11 -13
View File
@@ -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<Npc> npcRepository = new MobRepository<>(WorldConstants.MAXIMUM_NPCS);
/**
* The Queue of Npcs that have yet to be removed from the repository.
*/
private final Queue<Npc> 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<Npc> queuedNpcs = new ArrayDeque<>();
/**
* The Queue of Npcs that have yet to be removed from the repository.
*/
private final Queue<Npc> 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);
@@ -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 <a href="https://github.com/jruby/jruby/issues/2359">2359</a>. This class
* should <strong>not</strong> 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;
}
}
@@ -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 <a href="https://github.com/jruby/jruby/issues/2359">2359</a>.
*
* @author Major
*/
public final class ProxyEventListener implements EventListener<ProxyEvent> {
/**
* The {@link Map} from Event names to {@link List}s of {@link EventListener}s.
*/
private final Map<String, List<EventListener<Event>>> listeners = new HashMap<>();
/**
* Registers an {@link EventListener} to this proxy. This method is <strong>not</strong> 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<Event> listener) {
List<EventListener<Event>> listeners = this.listeners.computeIfAbsent(name, n -> new ArrayList<>(2));
listeners.add(listener);
}
@Override
@SuppressWarnings("unchecked")
public void handle(ProxyEvent event) {
List<EventListener<Event>> chain = listeners.get(event.getName());
if (chain != null) {
for (EventListener<Event> listener : chain) {
Event ruby = event.getRuby();
listener.handle(ruby);
if (ruby.terminated()) {
event.terminate();
break;
}
}
}
}
}
@@ -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);
}
}