diff --git a/README.md b/README.md index d581445d93..28ef99b2fd 100644 --- a/README.md +++ b/README.md @@ -5,14 +5,18 @@ This document is currently being worked on, so it may not be fully accurate. ## Beware -***This emulator is not production ready.*** +***This emulator is not production ready.*** -It can be useful for testing things locally or for trying out ideas, but launching a new private server based on this with no real changes is not recommended. +It can be useful for testing things locally or for trying out ideas, but launching a new private server based on this +with no real changes is not recommended. --- + ### Development information -#### Status -The current status is: *in development and gladly accepting contributions* + +#### Status (updated 28/9/21) + +Development is **on pause**, but any submitted PRs will be reviewed. #### Ways to contribute @@ -21,6 +25,7 @@ The current status is: *in development and gladly * Spread the word about Cosmic #### Community + GitHub: https://github.com/P0nk/Cosmic Discord: https://discord.gg/JU5aQapVZK diff --git a/src/main/java/net/server/channel/handlers/PlayerInteractionHandler.java b/src/main/java/net/server/channel/handlers/PlayerInteractionHandler.java index 75e90ae749..8fdfa362cb 100644 --- a/src/main/java/net/server/channel/handlers/PlayerInteractionHandler.java +++ b/src/main/java/net/server/channel/handlers/PlayerInteractionHandler.java @@ -83,7 +83,9 @@ public final class PlayerInteractionHandler extends AbstractPacketHandler { MERCHANT_MESO(0x2B), SOMETHING(0x2D), VIEW_VISITORS(0x2E), - BLACKLIST(0x2F), + VIEW_BLACKLIST(0x2F), + ADD_TO_BLACKLIST(0x30), + REMOVE_FROM_BLACKLIST(0x31), REQUEST_TIE(0x32), ANSWER_TIE(0x33), GIVE_UP(0x34), @@ -283,8 +285,7 @@ public final class PlayerInteractionHandler extends AbstractPacketHandler { int oid = p.readInt(); MapObject ob = chr.getMap().getMapObject(oid); - if (ob instanceof PlayerShop) { - PlayerShop shop = (PlayerShop) ob; + if (ob instanceof PlayerShop shop) { shop.visitShop(chr); } else if (ob instanceof MiniGame) { p.skip(1); @@ -309,8 +310,7 @@ public final class PlayerInteractionHandler extends AbstractPacketHandler { } else { chr.sendPacket(PacketCreator.getMiniRoomError(22)); } - } else if (ob instanceof HiredMerchant && chr.getHiredMerchant() == null) { - HiredMerchant merchant = (HiredMerchant) ob; + } else if (ob instanceof HiredMerchant merchant && chr.getHiredMerchant() == null) { merchant.visitShop(chr); } } @@ -681,6 +681,34 @@ public final class PlayerInteractionHandler extends AbstractPacketHandler { } merchant.withdrawMesos(chr); + + } else if (mode == Action.VIEW_VISITORS.getCode()) { + HiredMerchant merchant = chr.getHiredMerchant(); + if (merchant == null || !merchant.isOwner(chr)) { + return; + } + c.sendPacket(PacketCreator.viewMerchantVisitorHistory(merchant.getVisitorHistory())); + } else if (mode == Action.VIEW_BLACKLIST.getCode()) { + HiredMerchant merchant = chr.getHiredMerchant(); + if (merchant == null || !merchant.isOwner(chr)) { + return; + } + + c.sendPacket(PacketCreator.viewMerchantBlacklist(merchant.getBlacklist())); + } else if (mode == Action.ADD_TO_BLACKLIST.getCode()) { + HiredMerchant merchant = chr.getHiredMerchant(); + if (merchant == null || !merchant.isOwner(chr)) { + return; + } + String chrName = p.readString(); + merchant.addToBlacklist(chrName); + } else if (mode == Action.REMOVE_FROM_BLACKLIST.getCode()) { + HiredMerchant merchant = chr.getHiredMerchant(); + if (merchant == null || !merchant.isOwner(chr)) { + return; + } + String chrName = p.readString(); + merchant.removeFromBlacklist(chrName); } else if (mode == Action.MERCHANT_ORGANIZE.getCode()) { HiredMerchant merchant = chr.getHiredMerchant(); if (merchant == null || !merchant.isOwner(chr)) { diff --git a/src/main/java/server/maps/HiredMerchant.java b/src/main/java/server/maps/HiredMerchant.java index 473bfb435f..dfd9bc9668 100644 --- a/src/main/java/server/maps/HiredMerchant.java +++ b/src/main/java/server/maps/HiredMerchant.java @@ -45,10 +45,9 @@ import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; +import java.time.Duration; +import java.time.Instant; +import java.util.*; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.locks.Lock; @@ -57,6 +56,9 @@ import java.util.concurrent.locks.Lock; * @author Ronan - concurrency protection */ public class HiredMerchant extends AbstractMapObject { + private static final int VISITOR_HISTORY_LIMIT = 10; + private static final int BLACKLIST_LIMIT = 20; + private final int ownerId; private final int itemId; private final int mesos = 0; @@ -65,15 +67,21 @@ public class HiredMerchant extends AbstractMapObject { private final long start; private String ownerName = ""; private String description = ""; - private final Character[] visitors = new Character[3]; private final List items = new LinkedList<>(); private final List> messages = new LinkedList<>(); private final List sold = new LinkedList<>(); private final AtomicBoolean open = new AtomicBoolean(); private boolean published = false; private MapleMap map; + private final Visitor[] visitors = new Visitor[3]; + private final LinkedList visitorHistory = new LinkedList<>(); + private final LinkedHashSet blacklist = new LinkedHashSet<>(); // case-sensitive character names private final Lock visitorLock = MonitoredReentrantLockFactory.createLock(MonitoredLockType.VISITOR_MERCH, true); + private record Visitor(Character chr, Instant enteredAt) {} + + public record PastVisitor(String chrName, Duration visitDuration) {} + public HiredMerchant(final Character owner, String desc, int itemId) { this.setPosition(owner.getPosition()); this.start = System.currentTimeMillis(); @@ -96,9 +104,9 @@ public class HiredMerchant extends AbstractMapObject { } private void broadcastToVisitors(Packet packet) { - for (Character visitor : visitors) { + for (Visitor visitor : visitors) { if (visitor != null) { - visitor.sendPacket(packet); + visitor.chr.sendPacket(packet); } } } @@ -108,7 +116,7 @@ public class HiredMerchant extends AbstractMapObject { try { byte count = 0; if (this.isOpen()) { - for (Character visitor : visitors) { + for (Visitor visitor : visitors) { if (visitor != null) { count++; } @@ -128,7 +136,7 @@ public class HiredMerchant extends AbstractMapObject { try { int i = this.getFreeSlot(); if (i > -1) { - visitors[i] = visitor; + visitors[i] = new Visitor(visitor, Instant.now()); broadcastToVisitors(PacketCreator.hiredMerchantVisitorAdd(visitor, i + 1)); this.getMap().broadcastMessage(PacketCreator.updateHiredMerchantBox(this)); @@ -141,15 +149,18 @@ public class HiredMerchant extends AbstractMapObject { } } - public void removeVisitor(Character visitor) { + public void removeVisitor(Character chr) { visitorLock.lock(); try { - int slot = getVisitorSlot(visitor); + int slot = getVisitorSlot(chr); if (slot < 0) { //Not found return; } - if (visitors[slot] != null && visitors[slot].getId() == visitor.getId()) { + + Visitor visitor = visitors[slot]; + if (visitor != null && visitor.chr.getId() == chr.getId()) { visitors[slot] = null; + addVisitorToHistory(visitor); broadcastToVisitors(PacketCreator.hiredMerchantVisitorLeave(slot + 1)); this.getMap().broadcastMessage(PacketCreator.updateHiredMerchantBox(this)); } @@ -158,6 +169,14 @@ public class HiredMerchant extends AbstractMapObject { } } + private void addVisitorToHistory(Visitor visitor) { + Duration visitDuration = Duration.between(visitor.enteredAt, Instant.now()); + visitorHistory.addFirst(new PastVisitor(visitor.chr.getName(), visitDuration)); + while (visitorHistory.size() > VISITOR_HISTORY_LIMIT) { + visitorHistory.removeLast(); + } + } + public int getVisitorSlotThreadsafe(Character visitor) { visitorLock.lock(); try { @@ -169,7 +188,7 @@ public class HiredMerchant extends AbstractMapObject { private int getVisitorSlot(Character visitor) { for (int i = 0; i < 3; i++) { - if (visitors[i] != null && visitors[i].getId() == visitor.getId()) { + if (visitors[i] != null && visitors[i].chr.getId() == visitor.getId()) { return i; } } @@ -180,15 +199,15 @@ public class HiredMerchant extends AbstractMapObject { visitorLock.lock(); try { for (int i = 0; i < 3; i++) { - Character visitor = visitors[i]; + Visitor visitor = visitors[i]; if (visitor != null) { - visitor.setHiredMerchant(null); - - visitor.sendPacket(PacketCreator.leaveHiredMerchant(i + 1, 0x11)); - visitor.sendPacket(PacketCreator.hiredMerchantMaintenanceMessage()); - + final Character visitorChr = visitor.chr; + visitorChr.setHiredMerchant(null); + visitorChr.sendPacket(PacketCreator.leaveHiredMerchant(i + 1, 0x11)); + visitorChr.sendPacket(PacketCreator.hiredMerchantMaintenanceMessage()); visitors[i] = null; + addVisitorToHistory(visitor); } } @@ -468,6 +487,9 @@ public class HiredMerchant extends AbstractMapObject { } else if (!this.isOpen()) { chr.sendPacket(PacketCreator.getMiniRoomError(18)); return; + } else if (isBlacklisted(chr.getName())) { + chr.sendPacket(PacketCreator.getMiniRoomError(17)); + return; } else if (!this.addVisitor(chr)) { chr.sendPacket(PacketCreator.getMiniRoomError(2)); return; @@ -498,12 +520,15 @@ public class HiredMerchant extends AbstractMapObject { return description; } - public Character[] getVisitors() { + public Character[] getVisitorCharacters() { visitorLock.lock(); try { Character[] copy = new Character[3]; for (int i = 0; i < visitors.length; i++) { - copy[i] = visitors[i]; + Visitor visitor = visitors[i]; + if (visitor != null) { + copy[i] = visitor.chr; + } } return copy; @@ -694,6 +719,44 @@ public class HiredMerchant extends AbstractMapObject { } } + public List getVisitorHistory() { + return Collections.unmodifiableList(visitorHistory); + } + + public void addToBlacklist(String chrName) { + visitorLock.lock(); + try { + if (blacklist.size() >= BLACKLIST_LIMIT) { + return; + } + blacklist.add(chrName); + } finally { + visitorLock.unlock(); + } + } + + public void removeFromBlacklist(String chrName) { + visitorLock.lock(); + try { + blacklist.remove(chrName); + } finally { + visitorLock.unlock(); + } + } + + public Set getBlacklist() { + return Collections.unmodifiableSet(blacklist); + } + + private boolean isBlacklisted(String chrName) { + visitorLock.lock(); + try { + return blacklist.contains(chrName); + } finally { + visitorLock.unlock(); + } + } + public int getMapId() { return map.getId(); } diff --git a/src/main/java/tools/PacketCreator.java b/src/main/java/tools/PacketCreator.java index 0f92d43dae..7227677280 100644 --- a/src/main/java/tools/PacketCreator.java +++ b/src/main/java/tools/PacketCreator.java @@ -5089,7 +5089,7 @@ public class PacketCreator { p.writeInt(hm.getItemId()); p.writeString("Hired Merchant"); - Character[] visitors = hm.getVisitors(); + Character[] visitors = hm.getVisitorCharacters(); for (int i = 0; i < 3; i++) { if (visitors[i] != null) { p.writeByte(i + 1); @@ -5203,6 +5203,34 @@ public class PacketCreator { return p; } + /** + * @param pastVisitors Merchant visitors. The first 10 names will be shown, + * everything beyond will layered over each other at the top of the window. + */ + public static Packet viewMerchantVisitorHistory(List pastVisitors) { + final OutPacket p = OutPacket.create(SendOpcode.PLAYER_INTERACTION); + p.writeByte(PlayerInteractionHandler.Action.VIEW_VISITORS.getCode()); + p.writeShort(pastVisitors.size()); + for (HiredMerchant.PastVisitor pastVisitor : pastVisitors) { + p.writeString(pastVisitor.chrName()); + p.writeInt((int) pastVisitor.visitDuration().toMillis()); // milliseconds, displayed as hours and minutes + } + return p; + } + + /** + * @param chrNames Blacklisted names. The first 20 names will be displayed, anything beyond does no difference. + */ + public static Packet viewMerchantBlacklist(Set chrNames) { + final OutPacket p = OutPacket.create(SendOpcode.PLAYER_INTERACTION); + p.writeByte(PlayerInteractionHandler.Action.VIEW_BLACKLIST.getCode()); + p.writeShort(chrNames.size()); + for (String chrName : chrNames) { + p.writeString(chrName); + } + return p; + } + public static Packet hiredMerchantVisitorAdd(Character chr, int slot) { final OutPacket p = OutPacket.create(SendOpcode.PLAYER_INTERACTION); p.writeByte(PlayerInteractionHandler.Action.VISIT.getCode());