From a75fa743f819a27e4dad12383e3d6c002401d635 Mon Sep 17 00:00:00 2001 From: Ryley Kimmel Date: Sun, 12 Nov 2017 17:57:07 -0500 Subject: [PATCH] Initial implementation of ground items --- game/plugin/entity/ground-item/build.gradle | 5 ++ .../src/ground_item_action.plugin.kts | 13 ++++ .../ground-item/src/ground_item_sync.kt | 72 +++++++++++++++++++ .../entity/ground-item/src/ground_items.kt | 44 ++++++++++++ .../java/org/apollo/game/model/World.java | 26 ++++--- .../org/apollo/game/model/area/Region.java | 32 ++++++++- .../model/area/update/UpdateOperation.java | 21 +----- .../apollo/game/model/entity/GroundItem.java | 69 +++++++++++++++--- 8 files changed, 245 insertions(+), 37 deletions(-) create mode 100644 game/plugin/entity/ground-item/build.gradle create mode 100644 game/plugin/entity/ground-item/src/ground_item_action.plugin.kts create mode 100644 game/plugin/entity/ground-item/src/ground_item_sync.kt create mode 100644 game/plugin/entity/ground-item/src/ground_items.kt diff --git a/game/plugin/entity/ground-item/build.gradle b/game/plugin/entity/ground-item/build.gradle new file mode 100644 index 000000000..7ad357577 --- /dev/null +++ b/game/plugin/entity/ground-item/build.gradle @@ -0,0 +1,5 @@ +plugin { + name = "ground_item" + packageName = "org.apollo.game.plugin.entity" + authors = ["Ryley"] +} \ No newline at end of file diff --git a/game/plugin/entity/ground-item/src/ground_item_action.plugin.kts b/game/plugin/entity/ground-item/src/ground_item_action.plugin.kts new file mode 100644 index 000000000..ab207de20 --- /dev/null +++ b/game/plugin/entity/ground-item/src/ground_item_action.plugin.kts @@ -0,0 +1,13 @@ +import org.apollo.game.message.impl.InventoryItemMessage + +on { InventoryItemMessage::class } + .where { option == 5 } + .then { + // This is just a stub, for now. + // Several other things need to be done here: + // - Items may only be dropped from your inventory + // - Items dropped must be removed from your inventory + // - Items must be checked to ensure they have a 'drop' option + val item = it.inventory.get(slot) + it.addGroundItem(item, it.position) + } \ No newline at end of file diff --git a/game/plugin/entity/ground-item/src/ground_item_sync.kt b/game/plugin/entity/ground-item/src/ground_item_sync.kt new file mode 100644 index 000000000..24fded0ec --- /dev/null +++ b/game/plugin/entity/ground-item/src/ground_item_sync.kt @@ -0,0 +1,72 @@ +import org.apollo.game.GameConstants +import org.apollo.game.model.entity.GroundItem +import org.apollo.game.scheduling.ScheduledTask + +/** + * A [ScheduledTask] that manages the globalization and expiration of [GroundItem]s. + */ +class GroundItemSynchronizationTask(private val groundItem: GroundItem) : ScheduledTask(DELAY, false) { + + companion object { + + /** + * The delay between executions of this task, in pulses. + */ + const val DELAY = 1 + + /** + * The amount of time, in pulses, in which this [GroundItem] will be globally visible. + */ + const val TRADABLE_TIME_UNTIL_GLOBAL = 60000 / GameConstants.PULSE_DELAY + + /** + * The amount of time, in pulses, in which this [GroundItem] will expire and be removed from the [World]. + */ + const val UNTRADABLE_TIME_UNTIL_EXPIRE = 180000 / GameConstants.PULSE_DELAY + + /** + * The amount of time, in pulses, in which this [GroundItem] will expire and be removed from the [World]. + */ + const val TIME_UNTIL_EXPIRE = 180000 / GameConstants.PULSE_DELAY + } + + /** + * The amount of game pulses this [ScheduledTask] has been alive. + */ + private var pulses = 0 + + override fun execute() { + val world = groundItem.world + val owner = world.playerRepository[groundItem.ownerIndex] + val untradable = false // TODO: item.getDefinition().isTradable(); + + if (!groundItem.isAvailable) { + stop() + return + } + + // Untradable items never go global + if (untradable) { + if (pulses >= UNTRADABLE_TIME_UNTIL_EXPIRE) { + world.removeGroundItem(owner, groundItem) + stop() + } + return + } + + if (groundItem.isGlobal) { + if (pulses >= TIME_UNTIL_EXPIRE) { + world.removeGroundItem(owner, groundItem) + stop() + } + } else { + if (pulses >= TRADABLE_TIME_UNTIL_GLOBAL) { + groundItem.globalize() + world.addGroundItem(owner, groundItem) + } + } + + pulses++ + } + +} \ No newline at end of file diff --git a/game/plugin/entity/ground-item/src/ground_items.kt b/game/plugin/entity/ground-item/src/ground_items.kt new file mode 100644 index 000000000..e4c641bed --- /dev/null +++ b/game/plugin/entity/ground-item/src/ground_items.kt @@ -0,0 +1,44 @@ +import org.apollo.game.message.impl.RemoveTileItemMessage +import org.apollo.game.message.impl.SendTileItemMessage +import org.apollo.game.model.Item +import org.apollo.game.model.Position +import org.apollo.game.model.World +import org.apollo.game.model.entity.GroundItem +import org.apollo.game.model.entity.Player + +/** + * Spawns a new local [GroundItem] for this Player at the specified [Position]. + */ +fun Player.addGroundItem(item: Item, position: Position) { + world.addGroundItem(this, GroundItem.dropped(world, position, item, this)) +} + +internal fun World.addGroundItem(player: Player, item: GroundItem) { + val region = regionRepository.fromPosition(item.position) + + if (item.isGlobal) { + region.addEntity(item, true) + return + } + + groundItems.computeIfAbsent(player.encodedName, { HashSet() }) += item + + val offset = region.getPositionOffset(item) + player.send(SendTileItemMessage(item.item, offset)) + + schedule(GroundItemSynchronizationTask(item)) +} + +internal fun World.removeGroundItem(player: Player, item: GroundItem) { + val region = regionRepository.fromPosition(item.position) + + if (item.isGlobal) { + region.removeEntity(item) + } + + val items = groundItems[player.encodedName] ?: return + items -= item + + val offset = region.getPositionOffset(item) + player.send(RemoveTileItemMessage(item.item, offset)) +} \ No newline at end of file diff --git a/game/src/main/java/org/apollo/game/model/World.java b/game/src/main/java/org/apollo/game/model/World.java index 991a41fc6..ad2217495 100644 --- a/game/src/main/java/org/apollo/game/model/World.java +++ b/game/src/main/java/org/apollo/game/model/World.java @@ -1,8 +1,5 @@ package org.apollo.game.model; -import java.util.*; -import java.util.logging.Logger; - import com.google.common.base.Preconditions; import org.apollo.Service; import org.apollo.cache.IndexedFileSystem; @@ -19,11 +16,7 @@ import org.apollo.game.model.area.RegionRepository; import org.apollo.game.model.area.collision.CollisionManager; import org.apollo.game.model.area.collision.CollisionUpdateListener; -import org.apollo.game.model.entity.Entity; -import org.apollo.game.model.entity.EntityType; -import org.apollo.game.model.entity.MobRepository; -import org.apollo.game.model.entity.Npc; -import org.apollo.game.model.entity.Player; +import org.apollo.game.model.entity.*; import org.apollo.game.model.event.Event; import org.apollo.game.model.event.EventListener; import org.apollo.game.model.event.EventListenerChainSet; @@ -33,6 +26,9 @@ import org.apollo.game.scheduling.impl.NpcMovementTask; import org.apollo.util.NameUtil; +import java.util.*; +import java.util.logging.Logger; + /** * 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 @@ -96,6 +92,11 @@ public enum RegistrationStatus { */ private final MobRepository playerRepository = new MobRepository<>(WorldConstants.MAXIMUM_PLAYERS); + /** + * A {@link Map} of player usernames to their local {@link Set} of {@link GroundItem}s. + */ + private final Map> groundItems = new HashMap<>(WorldConstants.MAXIMUM_PLAYERS); + /** * A {@link Map} of player usernames and the player objects. */ @@ -181,6 +182,15 @@ public MobRepository getPlayerRepository() { return playerRepository; } + /** + * Gets the {@link Map} of player usernames to their {@link Set} of {@link GroundItem}s + * + * @return The map. + */ + public Map> getGroundItems() { + return groundItems; + } + /** * Gets the plugin manager. * diff --git a/game/src/main/java/org/apollo/game/model/area/Region.java b/game/src/main/java/org/apollo/game/model/area/Region.java index df7a9a170..baa2a4de1 100644 --- a/game/src/main/java/org/apollo/game/model/area/Region.java +++ b/game/src/main/java/org/apollo/game/model/area/Region.java @@ -12,6 +12,7 @@ import org.apollo.game.model.entity.Entity; import org.apollo.game.model.entity.EntityType; import org.apollo.game.model.entity.obj.DynamicGameObject; +import org.apollo.game.model.entity.obj.StaticGameObject; import java.util.ArrayList; import java.util.Collection; @@ -200,8 +201,8 @@ public boolean contains(Position position) { */ public Set encode(int height) { Set additions = entities.values().stream() - .flatMap(Set::stream) // TODO fix this to work for ground items + projectiles - .filter(entity -> entity instanceof DynamicGameObject && entity.getPosition().getHeight() == height) + .flatMap(Set::stream) // TODO: Stop hurting my eyeballs. + .filter(entity -> !entity.getEntityType().isMob() && !(entity instanceof StaticGameObject) && (!(entity instanceof DynamicGameObject) || entity.getPosition().getHeight() == height)) .map(entity -> ((GroupableEntity) entity).toUpdateOperation(this, EntityUpdateType.ADD).toMessage()) .collect(Collectors.toSet()); @@ -269,6 +270,33 @@ public Set getEntities(Position position, EntityType... ty return ImmutableSet.copyOf(filtered); } + /** + * Gets the position offset for the specified {@link Entity}. + * + * @param entity The Entity. + * @return The position offset. + */ + public int getPositionOffset(Entity entity) { + return getPositionOffset(entity.getPosition()); + } + + /** + * Gets the position offset for the specified {@link Position}. + * + * @param position The Entity. + * @return The position offset. + */ + public int getPositionOffset(Position position) { + RegionCoordinates coordinates = getCoordinates(); + int dx = position.getX() - coordinates.getAbsoluteX(); + int dy = position.getY() - coordinates.getAbsoluteY(); + + Preconditions.checkArgument(dx >= 0 && dx < Region.SIZE, position + " not in expected Region of " + toString() + "."); + Preconditions.checkArgument(dy >= 0 && dy < Region.SIZE, position + " not in expected Region of " + toString() + "."); + + return dx << 4 | dy; + } + /** * Gets the {@link Set} of {@link RegionCoordinates} of Regions that are viewable from the specified * {@link Position}. diff --git a/game/src/main/java/org/apollo/game/model/area/update/UpdateOperation.java b/game/src/main/java/org/apollo/game/model/area/update/UpdateOperation.java index 710eded25..43afcd911 100644 --- a/game/src/main/java/org/apollo/game/model/area/update/UpdateOperation.java +++ b/game/src/main/java/org/apollo/game/model/area/update/UpdateOperation.java @@ -53,7 +53,7 @@ public UpdateOperation(Region region, EntityUpdateType type, E entity) { * @return The RegionUpdateMessage. */ public final RegionUpdateMessage inverse() { - int offset = getPositionOffset(entity.getPosition()); + int offset = region.getPositionOffset(entity); switch (type) { case ADD: @@ -71,7 +71,7 @@ public final RegionUpdateMessage inverse() { * @return The Message. */ public final RegionUpdateMessage toMessage() { - int offset = getPositionOffset(entity.getPosition()); + int offset = region.getPositionOffset(entity); switch (type) { case ADD: @@ -99,21 +99,4 @@ public final RegionUpdateMessage toMessage() { */ protected abstract RegionUpdateMessage remove(int offset); - /** - * Gets the position offset for the specified {@link Position}. - * - * @param position The Position. - * @return The position offset. - */ - private final int getPositionOffset(Position position) { - RegionCoordinates coordinates = region.getCoordinates(); - int dx = position.getX() - coordinates.getAbsoluteX(); - int dy = position.getY() - coordinates.getAbsoluteY(); - - Preconditions.checkArgument(dx >= 0 && dx < Region.SIZE, position + " not in expected Region of " + region + "."); - Preconditions.checkArgument(dy >= 0 && dy < Region.SIZE, position + " not in expected Region of " + region + "."); - - return dx << 4 | dy; - } - } \ No newline at end of file diff --git a/game/src/main/java/org/apollo/game/model/entity/GroundItem.java b/game/src/main/java/org/apollo/game/model/entity/GroundItem.java index a81c612c1..9a17633df 100644 --- a/game/src/main/java/org/apollo/game/model/entity/GroundItem.java +++ b/game/src/main/java/org/apollo/game/model/entity/GroundItem.java @@ -9,6 +9,8 @@ import org.apollo.game.model.area.update.ItemUpdateOperation; import org.apollo.game.model.area.update.UpdateOperation; +import java.util.Objects; + /** * An {@link Item} displayed on the ground. * @@ -17,19 +19,19 @@ public final class GroundItem extends Entity implements GroupableEntity { /** - * Creates a new GroundItem. + * Creates a new global GroundItem. * * @param world The {@link World} containing the GroundItem. * @param position The {@link Position} of the Item. * @param item The Item displayed on the ground. * @return The GroundItem. */ - public static GroundItem create(World world, Position position, Item item) { - return new GroundItem(world, position, item, -1); + public static GroundItem createGlobal(World world, Position position, Item item) { + return new GroundItem(world, position, item, -1, true); } /** - * Creates a new dropped GroundItem. + * Creates a new non-global dropped GroundItem. * * @param world The {@link World} containing the GroundItem. * @param position The {@link Position} of the Item. @@ -38,7 +40,7 @@ public static GroundItem create(World world, Position position, Item item) { * @return The GroundItem. */ public static GroundItem dropped(World world, Position position, Item item, Player owner) { - return new GroundItem(world, position, item, owner.getIndex()); + return new GroundItem(world, position, item, owner.getIndex(), false); } /** @@ -51,6 +53,16 @@ public static GroundItem dropped(World world, Position position, Item item, Play */ private final Item item; + /** + * Represents the global state of this GroundItem. + */ + private boolean global; + + /** + * Represents the availability state of this GroundItem. + */ + private boolean available = true; + /** * Creates the GroundItem. * @@ -58,18 +70,20 @@ public static GroundItem dropped(World world, Position position, Item item, Play * @param position The {@link Position} of the GroundItem. * @param item The Item displayed on the ground. * @param index The index of the {@link Player} who dropped this GroundItem. + * @param global The global state of this GroundItem. */ - private GroundItem(World world, Position position, Item item, int index) { + private GroundItem(World world, Position position, Item item, int index, boolean global) { super(world, position); this.item = item; this.index = index; + this.global = global; } @Override public boolean equals(Object obj) { if (obj instanceof GroundItem) { GroundItem other = (GroundItem) obj; - return position.equals(other.position); + return item.equals(other.item) && position.equals(other.position) && global == other.global; } return false; @@ -99,9 +113,48 @@ public int getOwnerIndex() { return index; } + /** + * Gets the global state of this GroundItem. + * + * @return {@code true} iff this GroundItem is global. + */ + public boolean isGlobal() { + return global; + } + + /** + * Globalizes this GroundItem. + * + * @throws IllegalStateException If this GroundItem is already global. + */ + public void globalize() { + if (global) { + throw new IllegalStateException("Ground item state has already been set to global."); + } + global = true; + } + + /** + * Gets the availability of this GroundItem. + * + * @return {@code true} iff this GroundItem is available to be picked up. + */ + public boolean isAvailable() { + return available; + } + + /** + * Sets whether or not this GroundItem is available. + * + * @param available {@code true} if this GroundItem is available to be picked up, otherwise {@code false}. + */ + public void setAvailable(boolean available) { + this.available = available; + } + @Override public int hashCode() { - return position.hashCode() * 31 + item.hashCode(); + return Objects.hash(item, position, global); } @Override