From f2aced1bca4e6887a94d0ed0c275111d73c27e86 Mon Sep 17 00:00:00 2001 From: Gary Tierney Date: Wed, 2 Mar 2016 20:05:24 +0000 Subject: [PATCH] Add support for projectiles * Adds support for short-lived entities, which are never added to the entity sets of the region they belong in. * Adds support for checking the number of tiles a Mob occupies. * Adds a ProjectileBuilder class to simplify creating projectiles. --- .../message/impl/SendProjectileMessage.java | 74 +++ .../org/apollo/game/model/area/Region.java | 24 +- .../update/ProjectileUpdateOperation.java | 35 ++ .../apollo/game/model/entity/EntityType.java | 11 +- .../org/apollo/game/model/entity/Mob.java | 9 + .../apollo/game/model/entity/Projectile.java | 431 ++++++++++++++++++ .../apollo/game/release/r377/Release377.java | 2 + .../r377/SendProjectileMessageEncoder.java | 37 ++ 8 files changed, 614 insertions(+), 9 deletions(-) create mode 100644 game/src/main/org/apollo/game/message/impl/SendProjectileMessage.java create mode 100644 game/src/main/org/apollo/game/model/area/update/ProjectileUpdateOperation.java create mode 100644 game/src/main/org/apollo/game/model/entity/Projectile.java create mode 100644 game/src/main/org/apollo/game/release/r377/SendProjectileMessageEncoder.java diff --git a/game/src/main/org/apollo/game/message/impl/SendProjectileMessage.java b/game/src/main/org/apollo/game/message/impl/SendProjectileMessage.java new file mode 100644 index 00000000..bf60f8e5 --- /dev/null +++ b/game/src/main/org/apollo/game/message/impl/SendProjectileMessage.java @@ -0,0 +1,74 @@ +package org.apollo.game.message.impl; + +import org.apollo.game.model.entity.Projectile; +import org.apollo.net.message.Message; + +/** + * A {@link Message} sent to the client to display a projectile in the world. + */ +public final class SendProjectileMessage extends RegionUpdateMessage { + + /** + * The {@link Projectile} to be sent to the client. + */ + private final Projectile projectile; + + /** + * The position offset. + */ + private final int positionOffset; + + /** + * Creates the {@code SendProjectileMessage}. + * + * @param projectile The projectile to be sent to the client. Must not be {@code null}. + * @param positionOffset The position offset. + */ + public SendProjectileMessage(Projectile projectile, int positionOffset) { + this.projectile = projectile; + this.positionOffset = positionOffset; + } + + /** + * Gets the projectile. + * + * @return The projectile to be sent to the client. + */ + public Projectile getProjectile() { + return projectile; + } + + /** + * Gets the position offset. + * + * @return The position offset. + */ + public int getPositionOffset() { + return positionOffset; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof SendProjectileMessage) { + SendProjectileMessage other = (SendProjectileMessage) obj; + + return projectile.equals(other.projectile) && positionOffset == other.positionOffset; + } + + return false; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + positionOffset; + result = prime * result + projectile.hashCode(); + return result; + } + + @Override + public int priority() { + return LOW_PRIORITY; + } +} diff --git a/game/src/main/org/apollo/game/model/area/Region.java b/game/src/main/org/apollo/game/model/area/Region.java index 4334fcd5..1e66b211 100644 --- a/game/src/main/org/apollo/game/model/area/Region.java +++ b/game/src/main/org/apollo/game/model/area/Region.java @@ -136,11 +136,14 @@ public final class Region { * @throws IllegalArgumentException If the Entity does not belong in this Region. */ public void addEntity(Entity entity, boolean notify) { + EntityType type = entity.getEntityType(); Position position = entity.getPosition(); checkPosition(position); - Set local = entities.computeIfAbsent(position, key -> new HashSet<>(DEFAULT_LIST_SIZE)); - local.add(entity); + if (!type.isTransient()) { + Set local = entities.computeIfAbsent(position, key -> new HashSet<>(DEFAULT_LIST_SIZE)); + local.add(entity); + } if (notify) { notifyListeners(entity, EntityUpdateType.ADD); @@ -325,6 +328,12 @@ public final class Region { * @throws IllegalArgumentException If the Entity does not belong in this Region, or if it was never added. */ public void removeEntity(Entity entity) { + EntityType type = entity.getEntityType(); + if (type.isTransient()) { + throw new IllegalArgumentException("Tried to remove a transient Entity (" + entity + ") from " + + "(" + this + ")."); + } + Position position = entity.getPosition(); checkPosition(position); @@ -389,14 +398,13 @@ public final class Region { if (update == EntityUpdateType.REMOVE) { removedObjects.get(height).add(message); } else { // TODO should this really be possible? - removedObjects.get(height).remove(inverse); + removedObjects.get(height).remove(operation.inverse()); } - - updates.add(message); - } else { - updates.add(message); - updates.remove(inverse); + } else if (update == EntityUpdateType.REMOVE && !type.isTransient()) { + updates.remove(operation.inverse()); } + + updates.add(message); } } \ No newline at end of file diff --git a/game/src/main/org/apollo/game/model/area/update/ProjectileUpdateOperation.java b/game/src/main/org/apollo/game/model/area/update/ProjectileUpdateOperation.java new file mode 100644 index 00000000..af755956 --- /dev/null +++ b/game/src/main/org/apollo/game/model/area/update/ProjectileUpdateOperation.java @@ -0,0 +1,35 @@ +package org.apollo.game.model.area.update; + +import org.apollo.game.message.impl.RegionUpdateMessage; +import org.apollo.game.message.impl.SendProjectileMessage; +import org.apollo.game.model.area.EntityUpdateType; +import org.apollo.game.model.area.Region; +import org.apollo.game.model.entity.Projectile; + +/** + * An {@link UpdateOperation} for addition of {@link Projectile}s. + */ +public final class ProjectileUpdateOperation extends UpdateOperation { + + /** + * Creates the ProjectileUpdateOperation. + * + * @param region The region in which the UpdateOperation occurred. Must not be {@code null}. + * @param type The type of {@link EntityUpdateType}. Must not be {@code null}. + * @param entity The {@link Entity} being added or removed. Must not be {@code null}. + */ + public ProjectileUpdateOperation(Region region, EntityUpdateType type, Projectile entity) { + super(region, type, entity); + } + + @Override + protected RegionUpdateMessage add(int offset) { + return new SendProjectileMessage(entity, offset); + } + + @Override + protected RegionUpdateMessage remove(int offset) { + throw new IllegalStateException("Projectiles cannot be removed."); + } + +} diff --git a/game/src/main/org/apollo/game/model/entity/EntityType.java b/game/src/main/org/apollo/game/model/entity/EntityType.java index 2b79b908..1a4985c7 100644 --- a/game/src/main/org/apollo/game/model/entity/EntityType.java +++ b/game/src/main/org/apollo/game/model/entity/EntityType.java @@ -2,7 +2,7 @@ package org.apollo.game.model.entity; /** * Represents a type of {@link Entity}. - * + * * @author Major */ public enum EntityType { @@ -46,4 +46,13 @@ public enum EntityType { return this == PLAYER || this == NPC; } + /** + * Returns whether or not this EntityType should be short-lived (i.e. not added to its {@link Region}s + * local objects). + * + * @return {@code true} if this EntityType is short-lived. + */ + public boolean isTransient() { + return this == PROJECTILE; + } } \ No newline at end of file diff --git a/game/src/main/org/apollo/game/model/entity/Mob.java b/game/src/main/org/apollo/game/model/entity/Mob.java index b5b1f0f8..0c06a050 100644 --- a/game/src/main/org/apollo/game/model/entity/Mob.java +++ b/game/src/main/org/apollo/game/model/entity/Mob.java @@ -466,6 +466,15 @@ public abstract class Mob extends Entity { blockSet.add(SynchronizationBlock.createForceChatBlock(message)); } + /** + * Gets the number of tiles this mob occupies. + * + * @return The number of tiles this mob occupies. + */ + public int size() { + return definition.map(NpcDefinition::getSize).orElse(1); + } + /** * Starts a new action, stopping the current one if it exists. * diff --git a/game/src/main/org/apollo/game/model/entity/Projectile.java b/game/src/main/org/apollo/game/model/entity/Projectile.java new file mode 100644 index 00000000..db5459d8 --- /dev/null +++ b/game/src/main/org/apollo/game/model/entity/Projectile.java @@ -0,0 +1,431 @@ +package org.apollo.game.model.entity; + +import com.google.common.base.Objects; +import com.google.common.base.Preconditions; +import org.apollo.game.model.Position; +import org.apollo.game.model.World; +import org.apollo.game.model.area.EntityUpdateType; +import org.apollo.game.model.area.Region; +import org.apollo.game.model.area.update.GroupableEntity; +import org.apollo.game.model.area.update.ProjectileUpdateOperation; + +/** + * A projectile fired through the game world, such as an arrow. + * + * @author Major + */ +public final class Projectile extends Entity implements GroupableEntity { + + /** + * A builder for {@link Projectile}s. + */ + public static final class ProjectileBuilder { + + /** + * The time, in ticks, before the projectile is fired. + */ + private int delay; + + /** + * The ending {@link Position} of the Projectile. + */ + private Position destination; + + /** + * The ending height of the projectile. Defaults to the most commonly used ending height (for arrows and combat + * magic). + */ + private int endHeight = 40; + + /** + * The id of the graphic played as the Projectile proceeds. + */ + private int graphic; + + /** + * The time, in ticks, that the Projectile is present. + */ + private int lifetime; + + /** + * The pitch of the Projectile as it climbs. Defaults to the most commonly used pitch (for arrows and combat + * magic). + */ + private int pitch = 15; + + /** + * The starting {@link Position} of the Projectile. + */ + private Position source; + + /** + * The starting height of the projectile, relative to the height of the tile the Projectile starts on. Defaults + * to the most commonly used starting height (for arrows and combat magic). + */ + private int startHeight = 50; + + /** + * The index of the target Mob (which must be 0 if the Projectile does not target a Mob). + */ + private int target; + + /** + * The offset from the center of the {@code source} tile in world units. Defaults to 1/2 of + * a tile (or a {@link Mob} with a size of 1). + */ + private int offset = 64; + + /** + * The {@link World} the projectile will be built in. + */ + private final World world; + + /** + * Creates a new ProjectileBuilder. + * + * @param world The {@link World} the projectile will be built in. + */ + public ProjectileBuilder(World world) { + this.world = world; + } + + /** + * Builds this ProjectileBuilder into a {@link Projectile}. + * + * @return The built {@link Projectile}. Will never be {@code null}. + * @throws IllegalArgumentException If {@code lifetime} is not positive. + * @throws NullPointerException If the source or destination {@link Position}s are {@code null}. + */ + public Projectile build() { + Preconditions.checkNotNull(source, "Projectile must have a source."); + Preconditions.checkNotNull(destination, "Projectile must have a destination."); + Preconditions.checkArgument(lifetime > 0, "Lifetime must be positive."); + + return new Projectile(world, source, delay, destination, endHeight, graphic, lifetime, pitch, + startHeight, target, offset); + } + + /** + * Sets the delay before the Projectile is fired. + * + * @param delay The delay, in ticks. Must not be negative. + * @return This {@link ProjectileBuilder}, for chaining. Will never be {@code null}. + */ + public ProjectileBuilder delay(int delay) { + this.delay = delay; + return this; + } + + /** + * Sets the destination {@link Position} of the {@link Projectile}. + * + * @param destination The destination {@link Position}. Must not be {@code null}. + * @return This {@link ProjectileBuilder}, for chaining. Will never be {@code null}. + */ + public ProjectileBuilder destination(Position destination) { + this.destination = destination; + return this; + } + + /** + * Sets the end height of the {@link Projectile}. Note that this is the height in 'world units', + * not the height as described in {@link Position}s. + * + * @param height The end height of the {@link Projectile}. Must be {@code 0 <= height <= 255}. + * @return This {@link ProjectileBuilder}, for chaining. Will never be {@code null}. + */ + public ProjectileBuilder endHeight(int height) { + this.endHeight = height; + return this; + } + + /** + * Sets the id of the graphic played by the {@link Projectile}. + * + * @param graphic The graphic id. Must not be negative. + * @return This {@link ProjectileBuilder}, for chaining. Will never be {@code null}. + */ + public ProjectileBuilder graphic(int graphic) { + this.graphic = graphic; + return this; + } + + /** + * Sets the amount of time, in ticks, that the {@link Projectile} lasts in the game world. + * + * @param lifetime The lifetime of the {@link Projectile}, in ticks. Must not be negative. + * @return This {@link ProjectileBuilder}, for chaining. Will never be {@code null}. + */ + public ProjectileBuilder lifetime(int lifetime) { + this.lifetime = lifetime; + return this; + } + + /** + * Sets the pitch of the {@link Projectile}. + * + * @param pitch The pitch. + * @return This {@link ProjectileBuilder}, for chaining. Will never be {@code null}. + */ + public ProjectileBuilder pitch(int pitch) { + this.pitch = pitch; + return this; + } + + /** + * Sets the {@code offset} of the {@link Projectile} from the source tile. in 'world units'. + * + * @param offset The offset from the source tile in world units. + * @return This {@link ProjectileBuilder}, for chaining. Will never be {@code null}. + */ + public ProjectileBuilder offset(int offset) { + this.offset = offset; + return this; + } + + /** + * Sets the source {@link Position} of the {@link Projectile}. + * + * @param source The source {@link Projectile}. Must not be {@code null}. + * @return This {@link ProjectileBuilder}, for chaining. Will never be {@code null}. + */ + public ProjectileBuilder source(Position source) { + this.source = source; + return this; + } + + /** + * Sets the starting height of the {@link Projectile}, relative to the height of the origin tile (so a value + * of 0 fires the {@link Projectile} from the exact height of the origin tile). Note that this is the height + * in 'world units', not the height as described in {@link Position}s. + * + * @param height The start height of the {@link Projectile}. Must be {@code 0 <= height <= 255}. + * @return This {@link ProjectileBuilder}, for chaining. Will never be {@code null}. + */ + public ProjectileBuilder startHeight(int height) { + this.startHeight = height; + return this; + } + + /** + * Sets the target of the {@link Projectile}. + * + * @param mob The {@link Mob} the {@link Projectile} is targeting. Must not be {@code null}. + * @return This {@link ProjectileBuilder}, for chaining. Will never be {@code null}. + */ + public ProjectileBuilder target(Mob mob) { + int mobIndex = mob.getIndex() + 1; + + destination = mob.getPosition(); + target = mob.getEntityType() == EntityType.NPC ? mobIndex : -mobIndex; + offset = mob.size() * 64; + return this; + } + + } + + /** + * Creates a new {@link ProjectileBuilder}. + * + * @param world The {@link World} the Projectile will be created in. Must not be {@code null}. + * @return The {@link ProjectileBuilder}. Will never be {@code null}. + */ + public static ProjectileBuilder builder(World world) { + return new ProjectileBuilder(world); + } + + /** + * The time, in ticks, before the projectile is fired. + */ + private final int delay; + + /** + * The ending {@link Position} of the Projectile + */ + private final Position destination; + + /** + * The ending height of the projectile. + */ + private final int endHeight; + + /** + * The id of the graphic played as the Projectile proceeds. + */ + private final int graphic; + + /** + * The time, in ticks, that the Projectile is present. + */ + private final int lifetime; + + /** + * The pitch of the Projectile as it climbs. + */ + private final int pitch; + + /** + * The offset of the projectile from the origin in world units. + */ + private final int offset; + + /** + * The starting height of the projectile, relative to the height of the tile the Projectile starts on. + */ + private final int startHeight; + + /** + * The index of the target Mob (which must be 0 if the Projectile does not target a Mob). + */ + private final int target; + + /** + * Creates the Projectile. + * + * @param delay The time, in ticks, before the projectile is fired. Must not be negative. + * @param destination The ending {@link Position} of the Projectile. Must not be {@code null}. + * @param endHeight The ending height of the projectile. Must be [0, 255]. + * @param graphic The id of the graphic played as the Projectile proceeds. Must not be negative. + * @param lifetime The time, in ticks, that the Projectile is present. Must be positive. + * @param pitch The pitch of the Projectile as it climbs. + * @param source The starting {@link Position} of the Projectile. Must not be {@code null}. + * @param startHeight The starting height of the projectile, relative to the height of the {@code source} tile. + * @param target The index of the target Mob (or 0 if the Projectile is not targeting a Mob). + */ + public Projectile(World world, Position source, int delay, Position destination, int endHeight, int graphic, int lifetime, + int pitch, int startHeight, int target, int offset) { + super(world, source); + this.delay = delay; + this.destination = destination; + this.endHeight = endHeight; + this.graphic = graphic; + this.lifetime = lifetime; + this.pitch = pitch; + this.startHeight = startHeight; + this.target = target; + this.offset = offset; + } + + /** + * Gets the delay before this Projectile is fired, in ticks. + * + * @return The delay. + */ + public int getDelay() { + return delay; + } + + /** + * Gets the destination {@link Position} of this Projectile. + * + * @return The destination {@link Position}. Will never be {@code null}. + */ + public Position getDestination() { + return destination; + } + + /** + * Gets the ending height of this Projectile. + * + * @return The ending height. + */ + public int getEndHeight() { + return endHeight; + } + + /** + * Gets the id of the graphic played by this Projectile. + * + * @return The graphic id. + */ + public int getGraphic() { + return graphic; + } + + /** + * Gets the time, in ticks, that this Projectile lasts in the game world. + * + * @return The lifetime of this Projectile. + */ + public int getLifetime() { + return lifetime; + } + + /** + * Gets the offset of this Projectile. + * + * @return The offset of this Projectile. + */ + public int getOffset() { + return offset; + } + + /** + * Gets the pitch of this Projectile. + * + * @return The pitch. + */ + public int getPitch() { + return pitch; + } + + /** + * Gets the starting height of this Projectile, relative to the origin tile. + * + * @return The starting height. + */ + public int getStartHeight() { + return startHeight; + } + + /** + * Gets the index of the {@link Mob} this Projectile is targeting, or 0 if this Projectile is not targeting a + * {@link Mob}. + * + * @return The index. + */ + public int getTarget() { + return target; + } + + + @Override + public boolean equals(Object obj) { + if (obj instanceof Projectile) { + Projectile other = (Projectile) obj; + + return position.equals(other.position) + && destination.equals(other.destination) + && delay == other.delay + && lifetime == other.lifetime + && target == other.target + && startHeight == other.startHeight + && endHeight == other.endHeight + && pitch == other.pitch + && offset == other.offset + && graphic == other.graphic; + } + + return false; + } + + @Override + public EntityType getEntityType() { + return EntityType.PROJECTILE; + } + + @Override + public int hashCode() { + return Objects.hashCode(position, destination, delay, lifetime, target, startHeight, + endHeight, pitch, offset, graphic); + } + + @Override + public ProjectileUpdateOperation toUpdateOperation(Region region, EntityUpdateType type) { + Preconditions.checkArgument(type == EntityUpdateType.ADD, "Projectiles cannot be removed from the client"); + + return new ProjectileUpdateOperation(region, type, this); + } + +} + diff --git a/game/src/main/org/apollo/game/release/r377/Release377.java b/game/src/main/org/apollo/game/release/r377/Release377.java index 1ca1155e..74e83925 100644 --- a/game/src/main/org/apollo/game/release/r377/Release377.java +++ b/game/src/main/org/apollo/game/release/r377/Release377.java @@ -29,6 +29,7 @@ import org.apollo.game.message.impl.RemoveObjectMessage; import org.apollo.game.message.impl.RemoveTileItemMessage; import org.apollo.game.message.impl.SendFriendMessage; import org.apollo.game.message.impl.SendObjectMessage; +import org.apollo.game.message.impl.SendProjectileMessage; import org.apollo.game.message.impl.SendPublicTileItemMessage; import org.apollo.game.message.impl.SendTileItemMessage; import org.apollo.game.message.impl.ServerChatMessage; @@ -207,6 +208,7 @@ public final class Release377 extends Release { register(RemoveTileItemMessage.class, new RemoveTileItemMessageEncoder()); register(SendObjectMessage.class, new SendObjectMessageEncoder()); register(RemoveObjectMessage.class, new RemoveObjectMessageEncoder()); + register(SendProjectileMessage.class, new SendProjectileMessageEncoder()); register(GroupedRegionUpdateMessage.class, new GroupedRegionUpdateMessageEncoder(this)); register(ClearRegionMessage.class, new ClearRegionMessageEncoder()); diff --git a/game/src/main/org/apollo/game/release/r377/SendProjectileMessageEncoder.java b/game/src/main/org/apollo/game/release/r377/SendProjectileMessageEncoder.java new file mode 100644 index 00000000..1d470369 --- /dev/null +++ b/game/src/main/org/apollo/game/release/r377/SendProjectileMessageEncoder.java @@ -0,0 +1,37 @@ +package org.apollo.game.release.r377; + +import org.apollo.game.message.impl.SendProjectileMessage; +import org.apollo.game.model.Position; +import org.apollo.game.model.entity.Projectile; +import org.apollo.net.codec.game.DataType; +import org.apollo.net.codec.game.GamePacket; +import org.apollo.net.codec.game.GamePacketBuilder; +import org.apollo.net.release.MessageEncoder; + +/** + * A {@link MessageEncoder} for the {@link SendProjectileMessage}. + */ +public final class SendProjectileMessageEncoder extends MessageEncoder { + + @Override + public GamePacket encode(SendProjectileMessage message) { + Projectile projectile = message.getProjectile(); + Position source = projectile.getPosition(); + Position destination = projectile.getDestination(); + + GamePacketBuilder builder = new GamePacketBuilder(181); + builder.put(DataType.BYTE, message.getPositionOffset()); + builder.put(DataType.BYTE, destination.getX() - source.getX()); + builder.put(DataType.BYTE, destination.getY() - source.getY()); + builder.put(DataType.SHORT, projectile.getTarget()); + builder.put(DataType.SHORT, projectile.getGraphic()); + builder.put(DataType.BYTE, projectile.getStartHeight()); + builder.put(DataType.BYTE, projectile.getEndHeight()); + builder.put(DataType.SHORT, projectile.getDelay()); + builder.put(DataType.SHORT, projectile.getLifetime()); + builder.put(DataType.BYTE, projectile.getPitch()); + builder.put(DataType.BYTE, projectile.getOffset()); + return builder.toGamePacket(); + } + +}