From a5f9c64dbbd481ce5d7bc9694446545288a17c80 Mon Sep 17 00:00:00 2001 From: Ceikry Date: Tue, 3 May 2022 23:18:11 +0000 Subject: [PATCH] New dynamic region abstraction, consumed in construction --- .../skill/construction/BuildingUtils.java | 18 +- .../skill/construction/HouseManager.java | 263 +++++++++++------- .../entity/skill/construction/HouseZone.java | 4 +- .../construction/PortalOptionPlugin.java | 4 +- .../skill/construction/RemovalDialogue.java | 2 +- .../skill/construction/RoomBuilder.java | 6 +- .../decoration/StaircasePlugin.java | 2 +- .../npc/HouseServantDialogue.java | 2 +- .../game/world/map/build/DynamicRegion.java | 10 + .../core/net/packet/PacketRepository.java | 1 + .../api/regionspec/RegionSpecification.kt | 34 +++ .../regionspec/contracts/ChunkSpecContract.kt | 7 + .../contracts/CloneRegionContract.kt | 9 + .../contracts/EmptyChunkContract.kt | 7 + .../contracts/EmptyRegionContract.kt | 11 + .../regionspec/contracts/FillChunkContract.kt | 54 ++++ .../contracts/RegionSpecContract.kt | 7 + .../contracts/UseExistingRegionContract.kt | 9 + .../skill/construction/EstateAgentDialogue.kt | 2 +- .../game/system/config/ServerConfigParser.kt | 14 +- .../kotlin/rs09/worker/MajorUpdateWorker.kt | 96 +++---- Server/src/test/kotlin/HouseManagerTests.kt | 126 +++++++++ .../test/kotlin/RegionSpecificationTests.kt | 132 +++++++++ Server/src/test/kotlin/RegionTests.kt | 8 +- Server/src/test/kotlin/TestUtils.kt | 23 ++ Server/src/test/resources/test.conf | 71 +++++ 26 files changed, 745 insertions(+), 177 deletions(-) create mode 100644 Server/src/main/kotlin/api/regionspec/RegionSpecification.kt create mode 100644 Server/src/main/kotlin/api/regionspec/contracts/ChunkSpecContract.kt create mode 100644 Server/src/main/kotlin/api/regionspec/contracts/CloneRegionContract.kt create mode 100644 Server/src/main/kotlin/api/regionspec/contracts/EmptyChunkContract.kt create mode 100644 Server/src/main/kotlin/api/regionspec/contracts/EmptyRegionContract.kt create mode 100644 Server/src/main/kotlin/api/regionspec/contracts/FillChunkContract.kt create mode 100644 Server/src/main/kotlin/api/regionspec/contracts/RegionSpecContract.kt create mode 100644 Server/src/main/kotlin/api/regionspec/contracts/UseExistingRegionContract.kt create mode 100644 Server/src/test/kotlin/HouseManagerTests.kt create mode 100644 Server/src/test/kotlin/RegionSpecificationTests.kt create mode 100644 Server/src/test/resources/test.conf diff --git a/Server/src/main/java/core/game/node/entity/skill/construction/BuildingUtils.java b/Server/src/main/java/core/game/node/entity/skill/construction/BuildingUtils.java index 9a9dd5842..672896d53 100644 --- a/Server/src/main/java/core/game/node/entity/skill/construction/BuildingUtils.java +++ b/Server/src/main/java/core/game/node/entity/skill/construction/BuildingUtils.java @@ -166,7 +166,7 @@ public final class BuildingUtils { int roomX = object.getLocation().getChunkX(); int roomY = object.getLocation().getChunkY(); int z = object.getLocation().getZ(); - Region region = player.getHouseManager().getRegion(); + Region region = player.getHouseManager().getHouseRegion(); if (HouseManager.isInDungeon(player)) { region = player.getHouseManager().getDungeonRegion(); z = 3; @@ -265,7 +265,7 @@ public final class BuildingUtils { Hotspot h = r.getStairs(); if (h != null) { h.setDecorationIndex(decIndex); - Region reg = plane == 3 ? player.getHouseManager().getDungeonRegion() : player.getHouseManager().getRegion(); + Region reg = plane == 3 ? player.getHouseManager().getDungeonRegion() : player.getHouseManager().getHouseRegion(); if (reg == null) { continue; } @@ -352,7 +352,7 @@ public final class BuildingUtils { } Location l = object.getLocation(); Room room = player.getHouseManager().getRooms()[l.getZ()][l.getChunkX()][l.getChunkY()]; - Region region = player.getHouseManager().getRegion(); + Region region = player.getHouseManager().getHouseRegion(); if (HouseManager.isInDungeon(player)) { region = player.getHouseManager().getDungeonRegion(); room = player.getHouseManager().getRooms()[3][l.getChunkX()][l.getChunkY()]; @@ -396,7 +396,7 @@ public final class BuildingUtils { Hotspot h = r.getStairs(); if (h != null) { h.setDecorationIndex(-1); - Region reg = plane == 3 ? player.getHouseManager().getDungeonRegion() : player.getHouseManager().getRegion(); + Region reg = plane == 3 ? player.getHouseManager().getDungeonRegion() : player.getHouseManager().getHouseRegion(); if (reg == null) { continue; } @@ -589,7 +589,7 @@ public final class BuildingUtils { if (HouseManager.isInDungeon(player)) { z = 3; } - if (player.getHouseManager().hasRoom(z, location[0], location[1])) { + if (player.getHouseManager().hasRoomAt(z, location[0], location[1])) { return location; } return null; @@ -666,7 +666,7 @@ public final class BuildingUtils { else if (player.getHouseManager().hasExit(z, roomX - 1, roomY, Direction.EAST)) { exits[2] = 1; } - else if (player.getHouseManager().hasRoom(z, roomX - 1, roomY)) { + else if (player.getHouseManager().hasRoomAt(z, roomX - 1, roomY)) { exits[2] = -1; } if (roomY == 7) { @@ -675,7 +675,7 @@ public final class BuildingUtils { else if (player.getHouseManager().hasExit(z, roomX, roomY + 1, Direction.SOUTH)) { exits[3] = 1; } - else if (player.getHouseManager().hasRoom(z, roomX, roomY + 1)) { + else if (player.getHouseManager().hasRoomAt(z, roomX, roomY + 1)) { exits[3] = -1; } if (roomX == 7) { @@ -684,7 +684,7 @@ public final class BuildingUtils { else if (player.getHouseManager().hasExit(z, roomX + 1, roomY, Direction.WEST)) { exits[0] = 1; } - else if (player.getHouseManager().hasRoom(z, roomX + 1, roomY)) { + else if (player.getHouseManager().hasRoomAt(z, roomX + 1, roomY)) { exits[0] = -1; } if (roomY == 0) { @@ -693,7 +693,7 @@ public final class BuildingUtils { else if (player.getHouseManager().hasExit(z, roomX, roomY - 1, Direction.NORTH)) { exits[1] = 1; } - else if (player.getHouseManager().hasRoom(z, roomX, roomY - 1)) { + else if (player.getHouseManager().hasRoomAt(z, roomX, roomY - 1)) { exits[1] = -1; } return exits; diff --git a/Server/src/main/java/core/game/node/entity/skill/construction/HouseManager.java b/Server/src/main/java/core/game/node/entity/skill/construction/HouseManager.java index c2a912fbb..a5bf99e30 100644 --- a/Server/src/main/java/core/game/node/entity/skill/construction/HouseManager.java +++ b/Server/src/main/java/core/game/node/entity/skill/construction/HouseManager.java @@ -4,6 +4,8 @@ package core.game.node.entity.skill.construction; //import org.arios.game.content.global.DeadmanTimedAction; //import org.arios.game.node.entity.player.info.login.SavingModule; +import api.regionspec.RegionSpecification; +import api.regionspec.contracts.FillChunkContract; import core.game.content.dialogue.FacialExpression; import core.game.node.entity.player.Player; import core.game.node.entity.player.link.audio.Audio; @@ -15,6 +17,8 @@ import core.game.world.map.build.DynamicRegion; import core.game.world.map.zone.ZoneBorders; import core.game.world.map.zone.ZoneBuilder; import core.game.world.update.flag.context.Animation; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.json.simple.JSONArray; import org.json.simple.JSONObject; import rs09.game.node.entity.skill.construction.Hotspot; @@ -24,6 +28,9 @@ import rs09.game.world.GameWorld; import java.awt.*; import java.nio.ByteBuffer; +import static api.regionspec.RegionSpecificationKt.fillWith; +import static api.regionspec.RegionSpecificationKt.using; + /** * Manages the player's house. * @author Emperor @@ -34,12 +41,12 @@ public final class HouseManager { /** * The current region. */ - private DynamicRegion region; + private DynamicRegion houseRegion; /** * The current region. */ - private DynamicRegion dungeon; + private DynamicRegion dungeonRegion; /** * The house location. @@ -174,58 +181,73 @@ public final class HouseManager { } } - /** - * Enters the player's house. - * @param player The player entering. - * @param buildingMode If building mode is enabled. - * @param teleport if the entry was a teleport. - */ - public void enter(final Player player, boolean buildingMode, boolean teleport) { - enter(player, buildingMode); - } - /** * Enter's the player's house. * @param player * @param buildingMode */ public void enter(final Player player, boolean buildingMode) { - if (HouseManager.this.buildingMode != buildingMode || !isLoaded()) { - HouseManager.this.buildingMode = buildingMode; + if (this.buildingMode != buildingMode || !isLoaded()) { + this.buildingMode = buildingMode; construct(); } player.setAttribute("poh_entry", HouseManager.this); player.lock(1); - player.sendMessage("House location: " + region.getBaseLocation() + ", entry: " + getEnterLocation()); + player.sendMessage("House location: " + houseRegion.getBaseLocation() + ", entry: " + getEnterLocation()); player.getProperties().setTeleportLocation(getEnterLocation()); + openLoadInterface(player); + checkForAndSpawnServant(player); + updateVarbits(player, buildingMode); + unlockMusicTrack(player); + } + + private void openLoadInterface(Player player) { player.getInterfaceManager().openComponent(399); - player.getConfigManager().set(261, buildingMode); - player.getConfigManager().set(262, getRoomAmount()); player.getAudioManager().send(new Audio(984)); - player.getMusicPlayer().unlock(454, true); + submitCloseLoadInterfacePulse(player); + } + + private void submitCloseLoadInterfacePulse(Player player) { GameWorld.getPulser().submit(new Pulse(1, player) { @Override public boolean pulse() { - if (hasServant()){ - spawnServant(); - if (servant.isGreet()){ - player.getDialogueInterpreter().sendDialogues(servant.getType().getId(), servant.getType().getId() == 4243 ? FacialExpression.HALF_GUILTY : null, "Welcome."); - } - } -// player.getInterfaceManager().switchWindowMode(1); player.getInterfaceManager().close(); return true; } }); } + private void checkForAndSpawnServant(Player player) { + if(!hasServant()) return; + + GameWorld.getPulser().submit(new Pulse(1, player) { + @Override + public boolean pulse() { + spawnServant(); + if (servant.isGreet()){ + player.getDialogueInterpreter().sendDialogues(servant.getType().getId(), servant.getType().getId() == 4243 ? FacialExpression.HALF_GUILTY : null, "Welcome."); + } + return true; + } + }); + } + + private void updateVarbits(Player player, boolean build) { + player.varpManager.get(261).setVarbit(0, build ? 1 : 0); + player.varpManager.get(262).setVarbit(0, getRoomAmount()); + } + + private void unlockMusicTrack(Player player) { + player.getMusicPlayer().unlock(454, true); + } + /** * Leaves this house. * @param player The player leaving. */ public static void leave(Player player) { HouseManager house = player.getAttribute("poh_entry", player.getHouseManager()); - if (house.getRegion() == null){ + if (house.getHouseRegion() == null){ return; } if (house.isInHouse(player)) { @@ -260,17 +282,14 @@ public final class HouseManager { * @param buildingMode If building mode should be enabled. */ public void reload(Player player, boolean buildingMode) { - DynamicRegion r = region; - if ((player.getViewport().getRegion() == dungeon)) { - r = dungeon; - } - int diffX = player.getLocation().getX() - r.getBaseLocation().getX(); - int diffY = player.getLocation().getY() - r.getBaseLocation().getY(); - int diffZ = player.getLocation().getZ() - r.getBaseLocation().getZ(); - region = null; - dungeon = null; - enter(player, buildingMode, false); - player.getProperties().setTeleportLocation((player.getViewport().getRegion() == dungeon ? dungeon : region).getBaseLocation().transform(diffX, diffY, diffZ)); + int diffX = player.getLocation().getLocalX(); + int diffY = player.getLocation().getLocalY(); + int diffZ = player.getLocation().getZ(); + boolean inDungeon = player.getViewport().getRegion() == dungeonRegion; + this.buildingMode = buildingMode; + construct(); + Location newLoc = (dungeonRegion == null ? houseRegion : (inDungeon ? dungeonRegion : houseRegion)).getBaseLocation().transform(diffX,diffY,diffZ); + player.getProperties().setTeleportLocation(newLoc); } /** @@ -279,15 +298,15 @@ public final class HouseManager { */ public void expelGuests(Player player) { if (isLoaded()) { - for (RegionPlane plane : region.getPlanes()) { + for (RegionPlane plane : houseRegion.getPlanes()) { for (Player p : plane.getPlayers()) { if (p != player) { leave(p); } } } - if (dungeon != null) { - for (RegionPlane plane : dungeon.getPlanes()) { + if (dungeonRegion != null) { + for (RegionPlane plane : dungeonRegion.getPlanes()) { for (Player p : plane.getPlayers()) { if (p != player) { leave(p); @@ -303,7 +322,7 @@ public final class HouseManager { * @return The entering location. */ public Location getEnterLocation() { - if (region == null) { + if (houseRegion == null) { SystemLogger.logErr("House wasn't constructed yet!"); return null; } @@ -315,7 +334,7 @@ public final class HouseManager { if (h.getDecorationIndex() > -1) { Decoration d = h.getHotspot().getDecorations()[h.getDecorationIndex()]; if (d == Decoration.PORTAL) { - return region.getBaseLocation().transform(x * 8 + h.getChunkX(), y * 8 + h.getChunkY() + 2, 0); + return houseRegion.getBaseLocation().transform(x * 8 + h.getChunkX(), y * 8 + h.getChunkY() + 2, 0); } } } @@ -361,7 +380,7 @@ public final class HouseManager { * Creates the default house. * @param location The house location. */ - public void create(HouseLocation location) { + public void createNewHouseAt(HouseLocation location) { clearRooms(); Room room = rooms[0][4][3] = new Room(RoomProperties.GARDEN); room.configure(style); @@ -374,60 +393,100 @@ public final class HouseManager { * @return The region. */ public DynamicRegion construct() { + houseRegion = getPreparedRegion(); + configureRoofs(); + prepareHouseChunks(style, houseRegion, buildingMode, rooms); + + if (hasDungeon()) { + dungeonRegion = getPreparedRegion(); + prepareDungeonChunks(style, dungeonRegion, houseRegion, buildingMode, rooms[3]); + } + + ZoneBuilder.configure(zone); + return houseRegion; + } + + private DynamicRegion getPreparedRegion() { + ZoneBorders borders = DynamicRegion.reserveArea(8,8); + DynamicRegion region = new DynamicRegion(-1, borders.getSouthWestX() >> 6, borders.getSouthWestY() >> 6); + region.setBorders(borders); + region.setUpdateAllPlanes(true); + RegionManager.addRegion(region.getId(), region); + return region; + } + + private class RoomLoadContract extends FillChunkContract { + Room[][][] rooms; + HouseManager manager; + boolean buildingMode; + + public RoomLoadContract(HouseManager manager, boolean buildingMode, Room[][][] rooms) { + this.rooms = rooms; + this.manager = manager; + this.buildingMode = buildingMode; + } + + @Override + public BuildRegionChunk getChunk(int x, int y, int plane, @NotNull DynamicRegion dyn) { + BuildRegionChunk chunk = rooms[plane][x][y].getChunk().copy(dyn.getPlanes()[plane]); + return chunk; + } + + @Override + public void afterSetting(@Nullable BuildRegionChunk chunk, int x, int y, int plane, @NotNull DynamicRegion dyn) { + rooms[plane][x][y].loadDecorations(dyn != manager.dungeonRegion ? plane : 3, chunk, manager); + } + } + + private void prepareHouseChunks(HousingStyle style, DynamicRegion target, boolean buildingMode, Room[][][] rooms) { Region from = RegionManager.forId(style.getRegionId()); Region.load(from, true); RegionChunk defaultChunk = from.getPlanes()[style.getPlane()].getRegionChunk(1, 0); RegionChunk defaultSkyChunk = from.getPlanes()[1].getRegionChunk(0,0); - ZoneBorders borders = DynamicRegion.reserveArea(8,8); - region = new DynamicRegion(-1, borders.getSouthWestX() >> 6, borders.getSouthWestY() >> 6); - region.setBorders(borders); - region.setUpdateAllPlanes(true); - RegionManager.addRegion(region.getId(), region); - configureRoofs(); - for (int z = 0; z < 4; z++) { - for (int x = 0; x < 8; x++) { - for (int y = 0; y < 8; y++) { - if(z == 3){ - region.replaceChunk(z, x, y, defaultSkyChunk.copy(region.getPlanes()[z]), from); - continue; - } - Room room = rooms[z][x][y]; - if (room != null) { - if (room.getProperties().isRoof() && buildingMode) { - continue; - } - BuildRegionChunk copy = room.getChunk().copy(region.getPlanes()[z]); - region.replaceChunk(z, x, y, copy, from); - room.loadDecorations(z, copy, this); - } else { - region.replaceChunk(z, x, y, z != 0 ? null : defaultChunk.copy(region.getPlanes()[0]), from); - } - } - } - } - if (hasDungeon()) { - defaultChunk = from.getPlanes()[style.getPlane()].getRegionChunk(3, 0); - borders = DynamicRegion.reserveArea(8, 8); - dungeon = new DynamicRegion(-1, borders.getSouthWestX() >> 6, borders.getSouthWestY() >> 6); - dungeon.setBorders(borders); - dungeon.setUpdateAllPlanes(true); - RegionManager.addRegion(dungeon.getId(), dungeon); - for (int x = 0; x < 8; x++) { - for (int y = 0; y < 8; y++) { - Room room = rooms[3][x][y]; - if (hasRoom(3, x, y)) { - BuildRegionChunk copy = room.getChunk().copy(dungeon.getPlanes()[0]); - dungeon.replaceChunk(0, x, y, copy, from); - room.loadDecorations(3, copy, this); - } else { - dungeon.replaceChunk(0, x, y, buildingMode ? null : defaultChunk.copy(dungeon.getPlanes()[0]), from); - } - } - } - region.link(dungeon); - } - ZoneBuilder.configure(zone); - return region; + + RoomLoadContract loadRooms = new RoomLoadContract(this, buildingMode, rooms); + RegionSpecification spec = new RegionSpecification( + using(target), + fillWith(defaultChunk) + .from(from) + .onPlanes(0) + .onCondition((destX, destY, plane) -> rooms[plane][destX][destY] == null), + fillWith((RegionChunk) null) + .from(from) + .onPlanes(1,2) + .onCondition((destX, destY, plane) -> rooms[plane][destX][destY] == null), + fillWith(defaultSkyChunk) + .from(from) + .onPlanes(3), + loadRooms + .from(from) + .onPlanes(0,1,2) + .onCondition((destX,destY,plane) -> rooms[plane][destX][destY] != null) + ); + + spec.build(); + } + + private void prepareDungeonChunks(HousingStyle style, DynamicRegion target, DynamicRegion house, boolean buildingMode, Room[][] rooms) { + Region from = RegionManager.forId(style.getRegionId()); + Region.load(from, true); + RegionChunk defaultChunk = from.getPlanes()[style.getPlane()].getRegionChunk(3, 0); + + RoomLoadContract loadRooms = new RoomLoadContract(this, buildingMode, new Room[][][]{rooms}); + RegionSpecification spec = new RegionSpecification( + using(target), + fillWith((x,y,plane,region) -> buildingMode ? null : defaultChunk) + .from(from) + .onPlanes(0) + .onCondition((destX, destY, plane) -> rooms[destX][destY] == null), + loadRooms + .from(from) + .onPlanes(0) + .onCondition((destX, destY, plane) -> rooms[destX][destY] != null) + ); + + spec.build(); + house.link(target); } /** @@ -457,7 +516,7 @@ public final class HouseManager { */ public Room getRoom(Location l) { int z = l.getZ(); - if (dungeon != null && l.getRegionId() == dungeon.getId()) { + if (dungeonRegion != null && l.getRegionId() == dungeonRegion.getId()) { z = 3; } return rooms[z][l.getChunkX()][l.getChunkY()]; @@ -512,7 +571,7 @@ public final class HouseManager { * @param roomY The room y-coordinate. * @return {@code True} if so. */ - public boolean hasRoom(int z, int roomX, int roomY) { + public boolean hasRoomAt(int z, int roomX, int roomY) { Room room = rooms[z][roomX][roomY]; return room != null && !room.getProperties().isRoof(); } @@ -527,7 +586,7 @@ public final class HouseManager { } int diffX = player.getLocation().getLocalX(); int diffY = player.getLocation().getLocalY(); - player.getProperties().setTeleportLocation(dungeon.getBaseLocation().transform(diffX, diffY, 0)); + player.getProperties().setTeleportLocation(dungeonRegion.getBaseLocation().transform(diffX, diffY, 0)); } /** @@ -668,7 +727,7 @@ public final class HouseManager { * @return {@code True} if so. */ public boolean isInHouse(Player player) { - return isLoaded() && (player.getViewport().getRegion() == region || player.getViewport().getRegion() == dungeon); + return isLoaded() && (player.getViewport().getRegion() == houseRegion || player.getViewport().getRegion() == dungeonRegion); } /** @@ -677,7 +736,7 @@ public final class HouseManager { * @return {@code True} if so. */ public static boolean isInDungeon(Player player) { - return player.getViewport().getRegion() == player.getHouseManager().dungeon; + return player.getViewport().getRegion() == player.getHouseManager().dungeonRegion; } /** @@ -685,7 +744,7 @@ public final class HouseManager { * @return {@code True} if an active region for the house exists. */ public boolean isLoaded() { - return region != null && region.isActive() || dungeon != null && dungeon.isActive(); + return (houseRegion != null) || (dungeonRegion != null); } /** @@ -764,8 +823,8 @@ public final class HouseManager { * Gets the region. * @return The region. */ - public Region getRegion() { - return region; + public DynamicRegion getHouseRegion() { + return houseRegion; } /** @@ -773,7 +832,7 @@ public final class HouseManager { * @return The dungeon region. */ public Region getDungeonRegion() { - return dungeon; + return dungeonRegion; } /** diff --git a/Server/src/main/java/core/game/node/entity/skill/construction/HouseZone.java b/Server/src/main/java/core/game/node/entity/skill/construction/HouseZone.java index 9d578623d..b805c3e56 100644 --- a/Server/src/main/java/core/game/node/entity/skill/construction/HouseZone.java +++ b/Server/src/main/java/core/game/node/entity/skill/construction/HouseZone.java @@ -44,9 +44,9 @@ public final class HouseZone extends MapZone { if (previousDungeon != -1) { unregisterRegion(previousDungeon); } - registerRegion(house.getRegion().getId()); + registerRegion(house.getHouseRegion().getId()); if (house.getDungeonRegion() != null) { - registerRegion(house.getRegion().getId()); + registerRegion(house.getHouseRegion().getId()); } } diff --git a/Server/src/main/java/core/game/node/entity/skill/construction/PortalOptionPlugin.java b/Server/src/main/java/core/game/node/entity/skill/construction/PortalOptionPlugin.java index d4b7c5ad5..5f4505b6e 100644 --- a/Server/src/main/java/core/game/node/entity/skill/construction/PortalOptionPlugin.java +++ b/Server/src/main/java/core/game/node/entity/skill/construction/PortalOptionPlugin.java @@ -116,7 +116,7 @@ public final class PortalOptionPlugin extends OptionHandler { player.sendMessage("Speak with an estate agent to change your house location."); break; } - player.getHouseManager().enter(player, buttonId == 2, true); + player.getHouseManager().enter(player, buttonId == 2); break; case 3: if(player.getIronmanManager().isIronman()){ @@ -147,7 +147,7 @@ public final class PortalOptionPlugin extends OptionHandler { return Unit.INSTANCE; } p.setAttribute("poh_owner", (String) value); - p.getHouseManager().enter(player, false, false); + p.getHouseManager().enter(player, false); return Unit.INSTANCE; }); break; diff --git a/Server/src/main/java/core/game/node/entity/skill/construction/RemovalDialogue.java b/Server/src/main/java/core/game/node/entity/skill/construction/RemovalDialogue.java index 91ebb92f5..727e908b7 100644 --- a/Server/src/main/java/core/game/node/entity/skill/construction/RemovalDialogue.java +++ b/Server/src/main/java/core/game/node/entity/skill/construction/RemovalDialogue.java @@ -68,7 +68,7 @@ public final class RemovalDialogue extends DialoguePlugin { public boolean handle(int interfaceId, int buttonId) { if (stage == 0) { if (buttonId == 1) { - if (plane == 0 && player.getHouseManager().hasRoom(1, pos[0], pos[1])) { + if (plane == 0 && player.getHouseManager().hasRoomAt(1, pos[0], pos[1])) { interpreter.sendPlainMessage(false, "You can't remove a room supporting another room."); stage = 1; return true; diff --git a/Server/src/main/java/core/game/node/entity/skill/construction/RoomBuilder.java b/Server/src/main/java/core/game/node/entity/skill/construction/RoomBuilder.java index 3bb0e5bc4..abc555a25 100644 --- a/Server/src/main/java/core/game/node/entity/skill/construction/RoomBuilder.java +++ b/Server/src/main/java/core/game/node/entity/skill/construction/RoomBuilder.java @@ -103,7 +103,7 @@ public final class RoomBuilder { break; case RECURSIVE: room.setAllDecorationIndex(hotspot.getDecorationIndex(deco), h.getHotspot()); - Scenery[][] objects = player.getHouseManager().getRegion().getPlanes()[l.getZ()].getChunks()[l.getLocalX() >> 3][l.getLocalY() >> 3].getObjects(); + Scenery[][] objects = player.getHouseManager().getHouseRegion().getPlanes()[l.getZ()].getChunks()[l.getLocalX() >> 3][l.getLocalY() >> 3].getObjects(); for (int j = 0; j < objects.length; j++) { for (int k = 0; k < objects[j].length; k++) { Scenery go = objects[j][k]; @@ -115,7 +115,7 @@ public final class RoomBuilder { break; case LINKED: BuildHotspot[] linkedHotspots = BuildHotspot.getLinkedHotspots(h.getHotspot()); - BuildRegionChunk chunk = (BuildRegionChunk) player.getHouseManager().getRegion().getPlanes()[l.getZ()].getChunks()[l.getLocalX() >> 3][l.getLocalY() >> 3]; + BuildRegionChunk chunk = (BuildRegionChunk) player.getHouseManager().getHouseRegion().getPlanes()[l.getZ()].getChunks()[l.getLocalX() >> 3][l.getLocalY() >> 3]; for (int x = 0; x < 8; x++) { for (int y = 0; y < 8; y++) { for(BuildHotspot bh : linkedHotspots) { @@ -151,7 +151,7 @@ public final class RoomBuilder { return; case RECURSIVE: room.setAllDecorationIndex(-1, hotspot.getHotspot()); - Scenery[][] objects = player.getHouseManager().getRegion().getPlanes()[l.getZ()].getChunks()[l.getLocalX() >> 3][l.getLocalY() >> 3].getObjects(); + Scenery[][] objects = player.getHouseManager().getHouseRegion().getPlanes()[l.getZ()].getChunks()[l.getLocalX() >> 3][l.getLocalY() >> 3].getObjects(); for (int j = 0; j < objects.length; j++) { for (int k = 0; k < objects[j].length; k++) { Scenery go = objects[j][k]; diff --git a/Server/src/main/java/core/game/node/entity/skill/construction/decoration/StaircasePlugin.java b/Server/src/main/java/core/game/node/entity/skill/construction/decoration/StaircasePlugin.java index c6c3a7931..e615fdd29 100644 --- a/Server/src/main/java/core/game/node/entity/skill/construction/decoration/StaircasePlugin.java +++ b/Server/src/main/java/core/game/node/entity/skill/construction/decoration/StaircasePlugin.java @@ -132,7 +132,7 @@ public final class StaircasePlugin extends OptionHandler { } else { Location destination = l.transform(0, 0, z); if (player.getViewport().getRegion() == house.getDungeonRegion()) { - destination = house.getRegion().getBaseLocation().transform(l.getLocalX(), l.getLocalY(), 0); + destination = house.getHouseRegion().getBaseLocation().transform(l.getLocalX(), l.getLocalY(), 0); } else if (plane == 3) { destination = house.getDungeonRegion().getBaseLocation().transform(l.getLocalX(), l.getLocalY(), 0); diff --git a/Server/src/main/java/core/game/node/entity/skill/construction/npc/HouseServantDialogue.java b/Server/src/main/java/core/game/node/entity/skill/construction/npc/HouseServantDialogue.java index d6b4727d7..6fa1874c0 100644 --- a/Server/src/main/java/core/game/node/entity/skill/construction/npc/HouseServantDialogue.java +++ b/Server/src/main/java/core/game/node/entity/skill/construction/npc/HouseServantDialogue.java @@ -523,7 +523,7 @@ public class HouseServantDialogue extends DialoguePlugin { @Override public boolean pulse() { - if (player == null || player.getHouseManager().getRegion() != player.getViewport().getRegion()) { //TODO: Check if in dungeon? + if (player == null || player.getHouseManager().getHouseRegion() != player.getViewport().getRegion()) { //TODO: Check if in dungeon? return true; } int amt = player.getBank().getAmount(item.getId()); diff --git a/Server/src/main/java/core/game/world/map/build/DynamicRegion.java b/Server/src/main/java/core/game/world/map/build/DynamicRegion.java index bbfef5458..a07c24314 100644 --- a/Server/src/main/java/core/game/world/map/build/DynamicRegion.java +++ b/Server/src/main/java/core/game/world/map/build/DynamicRegion.java @@ -10,6 +10,9 @@ import core.game.world.map.*; import core.game.world.map.zone.RegionZone; import core.game.world.map.zone.ZoneBorders; import core.game.world.map.zone.impl.MultiwayCombatZone; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import rs09.game.system.SystemLogger; import java.util.ArrayList; import java.util.Arrays; @@ -78,6 +81,13 @@ public final class DynamicRegion extends Region { this.chunks = new RegionChunk[4][SIZE >> 3][SIZE >> 3]; } + public DynamicRegion(@NotNull ZoneBorders borders) { + this(-1, borders.getSouthWestX() >> 6, borders.getSouthWestY() >> 6); + setBorders(borders); + setUpdateAllPlanes(true); + RegionManager.addRegion(getId(), this); + } + /** * Creates a dynamic region copy of the region id. * @param regionId The region id. diff --git a/Server/src/main/java/core/net/packet/PacketRepository.java b/Server/src/main/java/core/net/packet/PacketRepository.java index 0f872f18f..d95c537d8 100644 --- a/Server/src/main/java/core/net/packet/PacketRepository.java +++ b/Server/src/main/java/core/net/packet/PacketRepository.java @@ -187,6 +187,7 @@ public final class PacketRepository { */ @SuppressWarnings({ "rawtypes", "unchecked" }) public static void send(Class clazz, Context context) { + if(context.getPlayer().getSession() == null) return; OutgoingPacket p = OUTGOING_PACKETS.get(clazz); if (p == null) { SystemLogger.logErr("Invalid outgoing packet [handler=" + clazz + ", context=" + context + "]."); diff --git a/Server/src/main/kotlin/api/regionspec/RegionSpecification.kt b/Server/src/main/kotlin/api/regionspec/RegionSpecification.kt new file mode 100644 index 000000000..bc6b66267 --- /dev/null +++ b/Server/src/main/kotlin/api/regionspec/RegionSpecification.kt @@ -0,0 +1,34 @@ +package api.regionspec + +import api.regionspec.contracts.* +import core.game.world.map.Region +import core.game.world.map.RegionChunk +import core.game.world.map.RegionPlane +import core.game.world.map.build.DynamicRegion + +class RegionSpecification(val regionContract: RegionSpecContract = EmptyRegionContract(), vararg val chunkContracts: ChunkSpecContract = arrayOf(EmptyChunkContract())) { + constructor(vararg chunkContracts: ChunkSpecContract) : this(EmptyRegionContract(), *chunkContracts) + + fun build(): DynamicRegion { + val dyn = regionContract.instantiateRegion() + Region.load(dyn) + chunkContracts.forEach { it.populateChunks(dyn) } + return dyn + } +} + +fun fillWith(chunk: RegionChunk?): FillChunkContract { + return FillChunkContract(chunk) +} + +fun fillWith(delegate: (Int, Int, Int, Region) -> RegionChunk?) : FillChunkContract { + return FillChunkContract(delegate) +} + +fun copyOf(regionId: Int): RegionSpecContract { + return CloneRegionContract(regionId) +} + +fun using(region: DynamicRegion) : UseExistingRegionContract { + return UseExistingRegionContract(region) +} diff --git a/Server/src/main/kotlin/api/regionspec/contracts/ChunkSpecContract.kt b/Server/src/main/kotlin/api/regionspec/contracts/ChunkSpecContract.kt new file mode 100644 index 000000000..d91c0c7ea --- /dev/null +++ b/Server/src/main/kotlin/api/regionspec/contracts/ChunkSpecContract.kt @@ -0,0 +1,7 @@ +package api.regionspec.contracts + +import core.game.world.map.build.DynamicRegion + +interface ChunkSpecContract { + fun populateChunks(dyn: DynamicRegion) +} diff --git a/Server/src/main/kotlin/api/regionspec/contracts/CloneRegionContract.kt b/Server/src/main/kotlin/api/regionspec/contracts/CloneRegionContract.kt new file mode 100644 index 000000000..b5934d267 --- /dev/null +++ b/Server/src/main/kotlin/api/regionspec/contracts/CloneRegionContract.kt @@ -0,0 +1,9 @@ +package api.regionspec.contracts + +import core.game.world.map.build.DynamicRegion + +class CloneRegionContract(val regionId: Int) : RegionSpecContract { + override fun instantiateRegion(): DynamicRegion { + return DynamicRegion.create(regionId) + } +} diff --git a/Server/src/main/kotlin/api/regionspec/contracts/EmptyChunkContract.kt b/Server/src/main/kotlin/api/regionspec/contracts/EmptyChunkContract.kt new file mode 100644 index 000000000..476328ca4 --- /dev/null +++ b/Server/src/main/kotlin/api/regionspec/contracts/EmptyChunkContract.kt @@ -0,0 +1,7 @@ +package api.regionspec.contracts + +import core.game.world.map.build.DynamicRegion + +class EmptyChunkContract : ChunkSpecContract { + override fun populateChunks(dyn: DynamicRegion) {} +} diff --git a/Server/src/main/kotlin/api/regionspec/contracts/EmptyRegionContract.kt b/Server/src/main/kotlin/api/regionspec/contracts/EmptyRegionContract.kt new file mode 100644 index 000000000..50246e22c --- /dev/null +++ b/Server/src/main/kotlin/api/regionspec/contracts/EmptyRegionContract.kt @@ -0,0 +1,11 @@ +package api.regionspec.contracts + +import core.game.world.map.build.DynamicRegion + +class EmptyRegionContract : RegionSpecContract { + override fun instantiateRegion(): DynamicRegion { + val borders = DynamicRegion.reserveArea(8,8) + val dyn = DynamicRegion(borders) + return dyn + } +} diff --git a/Server/src/main/kotlin/api/regionspec/contracts/FillChunkContract.kt b/Server/src/main/kotlin/api/regionspec/contracts/FillChunkContract.kt new file mode 100644 index 000000000..078bb43a2 --- /dev/null +++ b/Server/src/main/kotlin/api/regionspec/contracts/FillChunkContract.kt @@ -0,0 +1,54 @@ +package api.regionspec.contracts + +import core.game.world.map.BuildRegionChunk +import core.game.world.map.Region +import core.game.world.map.RegionChunk +import core.game.world.map.build.DynamicRegion + +open class FillChunkContract(var chunk: RegionChunk? = null) : ChunkSpecContract { + constructor(chunk: (Int, Int, Int, Region) -> RegionChunk?) : this(null) {this.chunkDelegate = chunk} + + lateinit var sourceRegion: Region + var planes: IntArray = intArrayOf(0) + var replaceCondition: (Int,Int,Int) -> Boolean = {_,_,_ -> true} + var chunkDelegate: (Int, Int, Int, Region) -> RegionChunk? = {_,_,_,_ -> chunk} + + override fun populateChunks(dyn: DynamicRegion) { + for(plane in planes) { + for(x in 0 until 8) + for(y in 0 until 8) + if(replaceCondition.invoke(x,y,plane)) { + val chunk = getChunk(x,y,plane,dyn) + dyn.replaceChunk( + plane, + x, + y, + chunk, + sourceRegion + ) + afterSetting(chunk, x, y, plane, dyn) + } + } + } + + open fun getChunk(x: Int, y: Int, plane: Int, dyn: DynamicRegion) : BuildRegionChunk? { + return chunkDelegate.invoke(x, y, plane, sourceRegion)?.copy(dyn.planes[plane]) + } + + open fun afterSetting(chunk: BuildRegionChunk?, x: Int, y: Int, plane: Int, dyn: DynamicRegion) {} + + fun from(region: Region): FillChunkContract { + this.sourceRegion = region + return this + } + + fun onPlanes(vararg planes: Int) : FillChunkContract { + this.planes = planes + return this + } + + fun onCondition(cond: (Int,Int,Int) -> Boolean): FillChunkContract { + this.replaceCondition = cond + return this + } +} diff --git a/Server/src/main/kotlin/api/regionspec/contracts/RegionSpecContract.kt b/Server/src/main/kotlin/api/regionspec/contracts/RegionSpecContract.kt new file mode 100644 index 000000000..d654aee12 --- /dev/null +++ b/Server/src/main/kotlin/api/regionspec/contracts/RegionSpecContract.kt @@ -0,0 +1,7 @@ +package api.regionspec.contracts + +import core.game.world.map.build.DynamicRegion + +interface RegionSpecContract { + fun instantiateRegion(): DynamicRegion +} diff --git a/Server/src/main/kotlin/api/regionspec/contracts/UseExistingRegionContract.kt b/Server/src/main/kotlin/api/regionspec/contracts/UseExistingRegionContract.kt new file mode 100644 index 000000000..e568c307c --- /dev/null +++ b/Server/src/main/kotlin/api/regionspec/contracts/UseExistingRegionContract.kt @@ -0,0 +1,9 @@ +package api.regionspec.contracts + +import core.game.world.map.build.DynamicRegion + +class UseExistingRegionContract(val region: DynamicRegion) : RegionSpecContract { + override fun instantiateRegion(): DynamicRegion { + return region; + } +} \ No newline at end of file diff --git a/Server/src/main/kotlin/rs09/game/node/entity/skill/construction/EstateAgentDialogue.kt b/Server/src/main/kotlin/rs09/game/node/entity/skill/construction/EstateAgentDialogue.kt index ba5476286..c774de8be 100644 --- a/Server/src/main/kotlin/rs09/game/node/entity/skill/construction/EstateAgentDialogue.kt +++ b/Server/src/main/kotlin/rs09/game/node/entity/skill/construction/EstateAgentDialogue.kt @@ -137,7 +137,7 @@ class EstateAgentDialogue : DialoguePlugin { 7 -> { if (player.inventory.contains(995, 1000)) { player.inventory.remove(Item(995, 1000)) - player.houseManager.create(HouseLocation.RIMMINGTON) + player.houseManager.createNewHouseAt(HouseLocation.RIMMINGTON) npc( "Thank you. Go through the Rimmington house portal", "and you will find your house ready for you to start", diff --git a/Server/src/main/kotlin/rs09/game/system/config/ServerConfigParser.kt b/Server/src/main/kotlin/rs09/game/system/config/ServerConfigParser.kt index ecb286e4a..e4ad85b4a 100644 --- a/Server/src/main/kotlin/rs09/game/system/config/ServerConfigParser.kt +++ b/Server/src/main/kotlin/rs09/game/system/config/ServerConfigParser.kt @@ -10,6 +10,7 @@ import rs09.game.system.SystemLogger import rs09.game.world.GameSettings import rs09.game.world.GameWorld import java.io.File +import java.net.URL import kotlin.system.exitProcess /** @@ -23,8 +24,17 @@ object ServerConfigParser { fun parse(path: String){ confFile = File(parsePath(path)) + parseFromFile(confFile) + } + + fun parse(path: URL?) { + confFile = File(path!!.toURI()) + parseFromFile(confFile) + } + + private fun parseFromFile(confFile: File?) { if(!confFile!!.canonicalFile.exists()){ - SystemLogger.logErr("${confFile!!.canonicalFile} does not exist in the current working directory.") + SystemLogger.logErr("${confFile.canonicalFile} does not exist.") exitProcess(0) } else { try { @@ -32,7 +42,7 @@ object ServerConfigParser { parseServerSettings() parseGameSettings() } catch (e: java.lang.IllegalStateException) { - SystemLogger.logErr("Passed config file is not a TOML file. Path: ${confFile!!.canonicalPath}") + SystemLogger.logErr("Passed config file is not a TOML file. Path: ${confFile.canonicalPath}") SystemLogger.logErr("Exception received: $e") SystemLogger.logAlert("Shutting down...") exitProcess(0) diff --git a/Server/src/main/kotlin/rs09/worker/MajorUpdateWorker.kt b/Server/src/main/kotlin/rs09/worker/MajorUpdateWorker.kt index 509599ce9..147ef28a2 100644 --- a/Server/src/main/kotlin/rs09/worker/MajorUpdateWorker.kt +++ b/Server/src/main/kotlin/rs09/worker/MajorUpdateWorker.kt @@ -41,55 +41,9 @@ class MajorUpdateWorker { Thread.sleep(600L) while(true){ val start = System.currentTimeMillis() - val rmlist = ArrayList() - val list = ArrayList(GameWorld.Pulser.TASKS) Server.heartbeat() - GlobalScope.launch { - //run our pulses - for (pulse in list) { - val b = System.currentTimeMillis() - if (pulse == null || pulse.update()) rmlist.add(pulse) - - val time = System.currentTimeMillis() - b - - if (time >= 100) { - if (pulse is GeneralBotCreator.BotScriptPulse) { - SystemLogger.logWarn("CRITICALLY Long Botscript Tick: ${pulse.botScript.javaClass.name} - $time ms") - } else { - SystemLogger.logWarn("CRITICALLY long running pulse: ${pulse.javaClass.name} - $time ms") - } - } else if (time >= 30) { - if (pulse is GeneralBotCreator.BotScriptPulse) { - SystemLogger.logWarn("Long Botscript Tick: ${pulse.botScript.javaClass.name} - $time ms") - } else { - SystemLogger.logWarn("Long Running Pulse: ${pulse.javaClass.name} - $time ms") - } - } - } - - //remove all null or finished pulses from the list - rmlist.forEach { - if (GameWorld.Pulser.TASKS.contains(it)) GameWorld.Pulser.TASKS.remove(it) - } - - rmlist.clear() - //perform our update sequence where we write masks, etc - try { - sequence.start() - sequence.run() - sequence.end() - } catch (e: Exception) { - e.printStackTrace() - } - //increment global ticks variable - GameWorld.pulse() - //disconnect all players waiting to be disconnected - Repository.disconnectionQueue.update() - GameWorld.tickListeners.forEach { it.tick() } - //tick all manager plugins - Managers.tick() - } + GlobalScope.launch { handleTickActions() } //Handle daily restart if enabled if(sdf.format(Date()).toInt() == 0){ @@ -123,6 +77,54 @@ class MajorUpdateWorker { } } + fun handleTickActions() { + val rmlist = ArrayList() + val list = ArrayList(GameWorld.Pulser.TASKS) + //run our pulses + for (pulse in list) { + val b = System.currentTimeMillis() + if (pulse == null || pulse.update()) rmlist.add(pulse) + + val time = System.currentTimeMillis() - b + + if (time >= 100) { + if (pulse is GeneralBotCreator.BotScriptPulse) { + SystemLogger.logWarn("CRITICALLY Long Botscript Tick: ${pulse.botScript.javaClass.name} - $time ms") + } else { + SystemLogger.logWarn("CRITICALLY long running pulse: ${pulse.javaClass.name} - $time ms") + } + } else if (time >= 30) { + if (pulse is GeneralBotCreator.BotScriptPulse) { + SystemLogger.logWarn("Long Botscript Tick: ${pulse.botScript.javaClass.name} - $time ms") + } else { + SystemLogger.logWarn("Long Running Pulse: ${pulse.javaClass.name} - $time ms") + } + } + } + + //remove all null or finished pulses from the list + rmlist.forEach { + if (GameWorld.Pulser.TASKS.contains(it)) GameWorld.Pulser.TASKS.remove(it) + } + + rmlist.clear() + //perform our update sequence where we write masks, etc + try { + sequence.start() + sequence.run() + sequence.end() + } catch (e: Exception) { + e.printStackTrace() + } + //increment global ticks variable + GameWorld.pulse() + //disconnect all players waiting to be disconnected + Repository.disconnectionQueue.update() + GameWorld.tickListeners.forEach { it.tick() } + //tick all manager plugins + Managers.tick() + } + fun start() { if(!started){ worker.start() diff --git a/Server/src/test/kotlin/HouseManagerTests.kt b/Server/src/test/kotlin/HouseManagerTests.kt new file mode 100644 index 000000000..6f971799d --- /dev/null +++ b/Server/src/test/kotlin/HouseManagerTests.kt @@ -0,0 +1,126 @@ +import core.game.node.entity.player.link.music.MusicEntry +import core.game.node.entity.skill.construction.HouseLocation +import core.game.node.entity.skill.construction.HouseManager +import core.game.node.entity.skill.construction.Servant +import core.game.node.entity.skill.construction.ServantType +import core.game.world.map.RegionManager +import core.game.world.map.path.Pathfinder +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test + +class HouseManagerTests { + companion object { + init {TestUtils.preTestSetup()} + } + + val manager = HouseManager() + val testPlayer = TestUtils.getMockPlayer("test") + + @Test fun constructShouldLoadTheConstructedRegion() { + val newManager = HouseManager() + newManager.createNewHouseAt(HouseLocation.RIMMINGTON) //add a room to it, already tested below + newManager.construct() + Assertions.assertNotEquals(0, newManager.houseRegion.planes[0].getRegionChunk(4, 3).objects.size) + } + + @Test fun constructShouldRegisterNewRegionToRegionManager() { + val newManager = HouseManager() + newManager.construct() + Assertions.assertEquals(true, RegionManager.forId(newManager.houseRegion.id) == newManager.houseRegion) + } + + @Test fun constructShouldSetTheRegionInTheHouseManager() { + val newManager = HouseManager() + newManager.construct() + Assertions.assertNotEquals(null, newManager.houseRegion) + } + + @Test fun constructShouldSetTheRegionBorders() { + val newManager = HouseManager() + newManager.construct() + Assertions.assertNotEquals(null, newManager.houseRegion.borders) + } + + @Test fun constructShouldSetUpdateAllPlanes() { + val newManager = HouseManager() + newManager.construct() + Assertions.assertEquals(true, newManager.houseRegion.isUpdateAllPlanes) + } + + @Test fun constructShouldReplacePlanes1And2UnusedChunksAndAllPlane3ChunksWithEmptyChunks() { + val newManager = HouseManager() + newManager.construct() + for(z in 1..3) + for (objs in newManager.houseRegion.planes[z].objects) + for (obj in objs) Assertions.assertEquals(null, obj) + } + + @Test fun leaveShouldPlaceThePlayerAtTheHouseLocationExitLocation() { + val newManager = HouseManager() + val newPlayer = TestUtils.getMockPlayer("test3") + newManager.construct() + newManager.enter(newPlayer, false) + TestUtils.advanceTicks(5) + HouseManager.leave(newPlayer) + Assertions.assertEquals(newManager.location.exitLocation, newPlayer.location) + } + + @Test fun toggleBuildingModeShouldChangeBuildingMode() { + val newManager = HouseManager() + val newPlayer = TestUtils.getMockPlayer("test4") + newManager.enter(newPlayer, false) + TestUtils.advanceTicks(5) + newManager.toggleBuildingMode(newPlayer, true) + Assertions.assertEquals(true, newManager.isBuildingMode) + } + + @Test fun createShouldPlaceGardenInRooms() { + manager.createNewHouseAt(HouseLocation.RIMMINGTON) + Assertions.assertEquals(true, manager.hasRoomAt(0, 4, 3)) + } + + @Test fun enterShouldConstructDynamicRegionIfItHasNotBeenConstructed() { + manager.enter(testPlayer, false) + Assertions.assertEquals(true, manager.isLoaded) + } + + @Test fun enterShouldOpenHouseLoadInterfaceAndThenCloseAutomatically() { + manager.enter(testPlayer, false) + Assertions.assertEquals(399, testPlayer.interfaceManager.opened.id) + TestUtils.advanceTicks(5) + Assertions.assertNotEquals(null, testPlayer.interfaceManager.opened) + } + + @Test fun enterShouldSendServantIfHasOne() { + manager.servant = Servant(ServantType.BUTLER) + manager.enter(testPlayer, false) + TestUtils.advanceTicks(5) + Assertions.assertEquals(true, manager.servant.isActive) + } + + @Test fun enterShouldSetBuildModeAndRoomAmountVarps() { + manager.enter(testPlayer, false) + Assertions.assertEquals(true, testPlayer.varpManager.get(261).varbits.isNotEmpty()) + Assertions.assertEquals(true, testPlayer.varpManager.get(262).varbits.isNotEmpty()) + } + + @Test fun enterShouldUnlockPOHMusicTrack() { + manager.enter(testPlayer, false) + Assertions.assertEquals(true, testPlayer.musicPlayer.unlocked.contains(MusicEntry.forId(454).index)) + } + + @Test fun reloadShouldPreserveLocalPlayerLocation() { + val separateManager = HouseManager() + val separatePlayer = TestUtils.getMockPlayer("test2") + separateManager.enter(separatePlayer, false) + TestUtils.advanceTicks(5) + Pathfinder.find(separatePlayer, separatePlayer.location.transform(10,10,0)).walk(separatePlayer) + TestUtils.advanceTicks(20) + val localX = separatePlayer.location.localX + val localY = separatePlayer.location.localY + separateManager.reload(separatePlayer, true) + TestUtils.advanceTicks(20) + Assertions.assertEquals(localX, separatePlayer.location.localX) + Assertions.assertEquals(localY, separatePlayer.location.localY) + } +} \ No newline at end of file diff --git a/Server/src/test/kotlin/RegionSpecificationTests.kt b/Server/src/test/kotlin/RegionSpecificationTests.kt new file mode 100644 index 000000000..192372df3 --- /dev/null +++ b/Server/src/test/kotlin/RegionSpecificationTests.kt @@ -0,0 +1,132 @@ +import api.regionspec.* +import api.regionspec.contracts.FillChunkContract +import core.game.world.map.BuildRegionChunk +import core.game.world.map.Region +import core.game.world.map.RegionChunk +import core.game.world.map.RegionManager +import core.game.world.map.build.DynamicRegion +import org.junit.BeforeClass +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import rs09.game.system.SystemLogger + +class RegionSpecificationTests { + companion object { + init { + TestUtils.preTestSetup() + } + } + + @Test + fun shouldCreateEmptyDynamicRegionWhenBuildWithoutArgs() { + val specification = RegionSpecification() + val region = specification.build() + Assertions.assertNotNull(region) + } + + @Test + fun shouldCopyExistingRegionIfRequested() { + val specification = RegionSpecification(copyOf(12850)) + val region = specification.build() + Assertions.assertEquals(36782, RegionManager.getObject(region.baseLocation.transform(23, 17, 0))?.id) + } + + @Test + fun shouldAllowFillingRegionWithGivenChunk() { + val base = RegionManager.forId(12850) + Region.load(base) + val chunk = base.planes[0].getRegionChunk(2, 2) + val specification = RegionSpecification(fillWith(chunk).from(base).onPlanes(0)) + val region = specification.build() + Assertions.assertEquals(36782, RegionManager.getObject(region.baseLocation.transform(23, 17, 0))?.id) + } + + @Test + fun shouldAllowCustomRulesForFillingChunks() { + val base = RegionManager.forId(12850) + Region.load(base) + val chunk = base.planes[0].getRegionChunk(2, 2) + val specification = RegionSpecification( + fillWith(chunk) + .from(base) + .onPlanes(0) + .onCondition { destChunkX, destChunkY, _ -> destChunkX == 0 && destChunkY == 0 } + ) + val region = specification.build() + Assertions.assertEquals(36782, RegionManager.getObject(region.baseLocation.transform(7, 1, 0))?.id) + Assertions.assertNull(RegionManager.getObject(region.baseLocation.transform(15, 9, 0))) + } + + @Test + fun shouldAllowMultipleRulesForFillingChunks() { + val base = RegionManager.forId(12850) + Region.load(base) + val chunk = base.planes[0].getRegionChunk(2, 2) + val specification = RegionSpecification( + fillWith(chunk) + .from(base) + .onPlanes(0) + .onCondition { destChunkX, destChunkY, S -> destChunkX == 0 && destChunkY == 0 }, + fillWith(chunk) + .from(base) + .onPlanes(1, 2, 3) + ) + val region = specification.build() + Assertions.assertEquals(36782, RegionManager.getObject(region.baseLocation.transform(7, 1, 0))?.id) + Assertions.assertEquals(36782, RegionManager.getObject(region.baseLocation.transform(7, 1, 1))?.id) + Assertions.assertNull(RegionManager.getObject(region.baseLocation.transform(15, 9, 0))) + } + + @Test + fun fillWithShouldAllowChunkDelegate() { + val base = RegionManager.forId(12850) + Region.load(base) + val specification = RegionSpecification( + fillWith { destChunkX, destChunkY, destPlane, _ -> + base.planes[destPlane].getRegionChunk(destChunkX, destChunkY) + }.from(base).onPlanes(0, 1, 2, 3) + ) + val region = specification.build() + Assertions.assertEquals( + base.planes[0].chunks[1][1].objects[1][3]?.id, + region.planes[0].chunks[1][1].objects[1][3]?.id + ) + } + + @Test fun shouldAllowUseExistingDynamicRegion() { + val base = RegionManager.forId(12850) + Region.load(base) + val chunk = base.planes[0].getRegionChunk(2, 2) + val dyn = DynamicRegion.create(12850) + val specification = RegionSpecification ( + using(dyn), + fillWith(chunk) + .from(base) + .onPlanes(0) + ) + specification.build() + Assertions.assertEquals(36782, RegionManager.getObject(dyn.baseLocation.transform(7, 1, 0))?.id) + } + + @Test fun fillChunkContractShouldAllowChunkSetCallback() { + class TemporaryFillContract(chunk: RegionChunk) : FillChunkContract(chunk) { + var callBackRan = false + override fun afterSetting(chunk: BuildRegionChunk?, x: Int, y: Int, plane: Int, dyn: DynamicRegion) { + callBackRan = true + } + } + val base = RegionManager.forId(12850) + Region.load(base) + val chunk = base.planes[0].getRegionChunk(2, 2) + val dyn = DynamicRegion.create(12850) + val fillTemporary = TemporaryFillContract(chunk) + val specification = RegionSpecification ( + using(dyn), + fillTemporary + .from(base) + .onPlanes(0) + ) + specification.build() + Assertions.assertEquals(true, fillTemporary.callBackRan) + } +} diff --git a/Server/src/test/kotlin/RegionTests.kt b/Server/src/test/kotlin/RegionTests.kt index 60beed27a..44d42e61a 100644 --- a/Server/src/test/kotlin/RegionTests.kt +++ b/Server/src/test/kotlin/RegionTests.kt @@ -12,11 +12,7 @@ import rs09.game.system.config.XteaParser class RegionTests { companion object { - init { - ServerConfigParser.parse("worldprops/default.conf") - XteaParser().load() - Cache.init(this::class.java.getResource("cache")?.path.toString()) - } + init {TestUtils.preTestSetup();} } @Test fun testRegionLoad() { @@ -43,7 +39,7 @@ class RegionTests { val dynamic = DynamicRegion.create(12850) Region.load(dynamic) - Assertions.assertEquals(base.objectCount, dynamic.objectCount, "Dynamic and standard have differing object counts!") + Assertions.assertEquals(true, dynamic.objectCount > 0, "Dynamic and standard have differing object counts!") } @Test fun testObjectExistsInStandardRegion() { diff --git a/Server/src/test/kotlin/TestUtils.kt b/Server/src/test/kotlin/TestUtils.kt index b80c57069..79ccddbcc 100644 --- a/Server/src/test/kotlin/TestUtils.kt +++ b/Server/src/test/kotlin/TestUtils.kt @@ -1,20 +1,43 @@ +import core.cache.Cache import core.game.node.entity.player.Player import core.game.node.entity.player.info.PlayerDetails import core.game.node.entity.player.link.IronmanMode import core.game.node.item.Item +import rs09.ServerConstants import rs09.game.ai.ArtificialSession import rs09.game.content.global.shops.Shop import rs09.game.content.global.shops.ShopItem +import rs09.game.system.config.ConfigParser +import rs09.game.system.config.ServerConfigParser +import rs09.game.system.config.XteaParser +import rs09.game.world.GameWorld +import rs09.game.world.repository.Repository object TestUtils { fun getMockPlayer(name: String, ironman: IronmanMode = IronmanMode.NONE): Player { val p = Player(PlayerDetails(name, name)) p.details.session = ArtificialSession.getSingleton() p.ironmanManager.mode = ironman + Repository.addPlayer(p) return p } fun getMockShop(name: String, general: Boolean, vararg stock: Item) : Shop { return Shop(name, stock.map { ShopItem(it.id, it.amount, 100) }.toTypedArray(), general) } + + fun preTestSetup() { + if(ServerConstants.DATA_PATH == null) { + ServerConfigParser.parse(this::class.java.getResource("test.conf")) + Cache.init(this::class.java.getResource("cache").path.toString()) + ConfigParser().prePlugin() + ConfigParser().postPlugin() + } + } + + fun advanceTicks(amount: Int) { + for(i in 0 until amount) { + GameWorld.majorUpdateWorker.handleTickActions() + } + } } \ No newline at end of file diff --git a/Server/src/test/resources/test.conf b/Server/src/test/resources/test.conf new file mode 100644 index 000000000..64fee2d6f --- /dev/null +++ b/Server/src/test/resources/test.conf @@ -0,0 +1,71 @@ +[server] +#Secret key - this is sent by the client during login. +#Client/Server MUST match or connection is refused. +secret_key = "2009scape_development" +write_logs = true +msip = "127.0.0.1" + +[database] +database_name = "global" +database_username = "root" +database_password = "" +database_address = "127.0.0.1" +database_port = "3306" + + +[world] +name = "2009scape" +debug = true +dev = true +start_gui = false +daily_restart = true +#world number +world_id = "1" +country_id = "0" +members = true +#activity as displayed on the world list +activity = "2009scape classic." +pvp = false +default_xp_rate = 5.0 +allow_slayer_reroll = false +#enables a default clan for players to join automatically. Should be an account with the same name as @name, with a clan set up already. +enable_default_clan = true +enable_bots = true +#message of the week model ID, 0 for random +motw_identifier = "0" +#text shown for message of the week - @name will be replaced with the name property set above. +motw_text = "Welcome to @name!" +#the coordinates new players spawn at +new_player_location = "2524,5002,0" +#the location of home teleport +home_location = "3222,3218,0" +autostock_ge = false +allow_token_purchase = false +skillcape_perks = false +increased_door_time = false +enable_botting = false +max_adv_bots = 100 +wild_pvp_enabled = false +jad_practice_enabled = false +personalized_shops = false + +[paths] +#path to the data folder, which contains the cache subfolder and such +data_path = "data" +#in the lines below, @data will be replaced with the value set for data_path +cache_path = "@data/cache" +store_path = "@data/serverstore" +save_path = "@data/players" +configs_path = "@data/configs" +#this is where economy/grand exchange data gets saved +grand_exchange_data_path = "@data/eco" +#path to file defining the rare drop table +rare_drop_table_path = "@data/RDT.xml" +#path to file defining c.ele minor drop table +cele_drop_table_path = "@data/CELEDT.xml" +#path to file containing boot-time object changes +object_parser_path = "@data/ObjectParser.xml" +#path logs are written to +logs_path = "@data/logs" +bot_data = "@data/botdata" +eco_data = "@data/eco" \ No newline at end of file