Merge pull request #58 from P0nk/merchant-lists
Merchant visitor list and blacklist
This commit is contained in:
13
README.md
13
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: <span style="color:LightGreen">*in development and gladly accepting contributions*</span>
|
||||
|
||||
#### Status (updated 28/9/21)
|
||||
|
||||
Development is <span style="color:Orange">**on pause**</span>, but any submitted PRs will be reviewed.
|
||||
|
||||
#### Ways to contribute
|
||||
|
||||
@@ -21,6 +25,7 @@ The current status is: <span style="color:LightGreen">*in development and gladly
|
||||
* Spread the word about Cosmic
|
||||
|
||||
#### Community
|
||||
|
||||
GitHub: https://github.com/P0nk/Cosmic
|
||||
Discord: https://discord.gg/JU5aQapVZK
|
||||
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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<PlayerShopItem> items = new LinkedList<>();
|
||||
private final List<Pair<String, Byte>> messages = new LinkedList<>();
|
||||
private final List<SoldItem> 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<PastVisitor> visitorHistory = new LinkedList<>();
|
||||
private final LinkedHashSet<String> 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<PastVisitor> 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<String> 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();
|
||||
}
|
||||
|
||||
@@ -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<HiredMerchant.PastVisitor> 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<String> 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());
|
||||
|
||||
Reference in New Issue
Block a user