Protected Trade/Quest system + Expirable quests

Added true protection against race conditions on player trades and fixed
some situational issues. Character's quest status table also received
concurrency treatment. Quests with time limit now expires properly.
Increased subtly the performance on the server start-up.
This commit is contained in:
ronancpl
2017-05-26 23:09:42 -03:00
parent 702c69897b
commit d0396e4c36
15 changed files with 229 additions and 132 deletions

View File

@@ -239,7 +239,7 @@ public class MapleCharacter extends AbstractAnimatedMapleMapObject {
private SavedLocation savedLocations[];
private SkillMacro[] skillMacros = new SkillMacro[5];
private List<Integer> lastmonthfameids;
private Map<Short, MapleQuestStatus> quests;
private final Map<Short, MapleQuestStatus> quests;
private Set<MapleMonster> controlled = new LinkedHashSet<>();
private Map<Integer, String> entered = new LinkedHashMap<>();
private Set<MapleMapObject> visibleMapObjects = new LinkedHashSet<>();
@@ -2067,13 +2067,16 @@ public class MapleCharacter extends AbstractAnimatedMapleMapObject {
}
public final List<MapleQuestStatus> getCompletedQuests() {
List<MapleQuestStatus> ret = new LinkedList<>();
for (MapleQuestStatus q : quests.values()) {
if (q.getStatus().equals(MapleQuestStatus.Status.COMPLETED)) {
ret.add(q);
synchronized (quests) {
List<MapleQuestStatus> ret = new LinkedList<>();
for (MapleQuestStatus q : quests.values()) {
if (q.getStatus().equals(MapleQuestStatus.Status.COMPLETED)) {
ret.add(q);
}
}
return Collections.unmodifiableList(ret);
}
return Collections.unmodifiableList(ret);
}
public Collection<MapleMonster> getControlledMonsters() {
@@ -2603,46 +2606,58 @@ public class MapleCharacter extends AbstractAnimatedMapleMapObject {
}
public final byte getQuestStatus(final int quest) {
for (final MapleQuestStatus q : quests.values()) {
if (q.getQuest().getId() == quest) {
return (byte) q.getStatus().getId();
synchronized (quests) {
for (final MapleQuestStatus q : quests.values()) {
if (q.getQuest().getId() == quest) {
return (byte) q.getStatus().getId();
}
}
return 0;
}
return 0;
}
public MapleQuestStatus getQuest(MapleQuest quest) {
if (!quests.containsKey(quest.getId())) {
return new MapleQuestStatus(quest, MapleQuestStatus.Status.NOT_STARTED);
synchronized (quests) {
if (!quests.containsKey(quest.getId())) {
return new MapleQuestStatus(quest, MapleQuestStatus.Status.NOT_STARTED);
}
return quests.get(quest.getId());
}
return quests.get(quest.getId());
}
//---- \/ \/ \/ \/ \/ \/ \/ NOT TESTED \/ \/ \/ \/ \/ \/ \/ \/ \/ ----
public final void setQuestAdd(final MapleQuest quest, final byte status, final String customData) {
if (!quests.containsKey(quest.getId())) {
final MapleQuestStatus stat = new MapleQuestStatus(quest, MapleQuestStatus.Status.getById((int)status));
stat.setCustomData(customData);
quests.put(quest.getId(), stat);
synchronized (quests) {
if (!quests.containsKey(quest.getId())) {
final MapleQuestStatus stat = new MapleQuestStatus(quest, MapleQuestStatus.Status.getById((int)status));
stat.setCustomData(customData);
quests.put(quest.getId(), stat);
}
}
}
public final MapleQuestStatus getQuestNAdd(final MapleQuest quest) {
if (!quests.containsKey(quest.getId())) {
final MapleQuestStatus status = new MapleQuestStatus(quest, MapleQuestStatus.Status.NOT_STARTED);
quests.put(quest.getId(), status);
return status;
synchronized (quests) {
if (!quests.containsKey(quest.getId())) {
final MapleQuestStatus status = new MapleQuestStatus(quest, MapleQuestStatus.Status.NOT_STARTED);
quests.put(quest.getId(), status);
return status;
}
return quests.get(quest.getId());
}
return quests.get(quest.getId());
}
public final MapleQuestStatus getQuestNoAdd(final MapleQuest quest) {
return quests.get(quest.getId());
synchronized (quests) {
return quests.get(quest.getId());
}
}
public final MapleQuestStatus getQuestRemove(final MapleQuest quest) {
return quests.remove(quest.getId());
synchronized (quests) {
return quests.remove(quest.getId());
}
}
//---- /\ /\ /\ /\ /\ /\ /\ NOT TESTED /\ /\ /\ /\ /\ /\ /\ /\ /\ ----
@@ -2742,26 +2757,30 @@ public class MapleCharacter extends AbstractAnimatedMapleMapObject {
}
public final List<MapleQuestStatus> getStartedQuests() {
List<MapleQuestStatus> ret = new LinkedList<>();
for (MapleQuestStatus q : quests.values()) {
if (q.getStatus().equals(MapleQuestStatus.Status.STARTED)) {
ret.add(q);
synchronized (quests) {
List<MapleQuestStatus> ret = new LinkedList<>();
for (MapleQuestStatus q : quests.values()) {
if (q.getStatus().equals(MapleQuestStatus.Status.STARTED)) {
ret.add(q);
}
}
return Collections.unmodifiableList(ret);
}
return Collections.unmodifiableList(ret);
}
public final int getStartedQuestsSize() {
int i = 0;
for (MapleQuestStatus q : quests.values()) {
if (q.getStatus().equals(MapleQuestStatus.Status.STARTED)) {
if (q.getQuest().getInfoNumber() > 0) {
synchronized (quests) {
int i = 0;
for (MapleQuestStatus q : quests.values()) {
if (q.getStatus().equals(MapleQuestStatus.Status.STARTED)) {
if (q.getQuest().getInfoNumber() > 0) {
i++;
}
i++;
}
i++;
}
return i;
}
return i;
}
public MapleStatEffect getStatForBuff(MapleBuffStat effect) {
@@ -3698,17 +3717,19 @@ public class MapleCharacter extends AbstractAnimatedMapleMapObject {
}
int lastQuestProcessed = 0;
try {
for (MapleQuestStatus q : quests.values()) {
lastQuestProcessed = q.getQuest().getId();
if (q.getStatus() == MapleQuestStatus.Status.COMPLETED || q.getQuest().canComplete(this, null)) {
continue;
}
String progress = q.getProgress(id);
if (!progress.isEmpty() && Integer.parseInt(progress) >= q.getQuest().getMobAmountNeeded(id)) {
continue;
}
if (q.progress(id)) {
client.announce(MaplePacketCreator.updateQuest(q, false));
synchronized (quests) {
for (MapleQuestStatus q : quests.values()) {
lastQuestProcessed = q.getQuest().getId();
if (q.getStatus() == MapleQuestStatus.Status.COMPLETED || q.getQuest().canComplete(this, null)) {
continue;
}
String progress = q.getProgress(id);
if (!progress.isEmpty() && Integer.parseInt(progress) >= q.getQuest().getMobAmountNeeded(id)) {
continue;
}
if (q.progress(id)) {
client.announce(MaplePacketCreator.updateQuest(q, false));
}
}
}
} catch (Exception e) {
@@ -4536,27 +4557,30 @@ public class MapleCharacter extends AbstractAnimatedMapleMapObject {
try (PreparedStatement pse = con.prepareStatement("INSERT INTO questprogress VALUES (DEFAULT, ?, ?, ?)")) {
psf = con.prepareStatement("INSERT INTO medalmaps VALUES (DEFAULT, ?, ?)");
ps.setInt(1, id);
for (MapleQuestStatus q : quests.values()) {
ps.setInt(2, q.getQuest().getId());
ps.setInt(3, q.getStatus().getId());
ps.setInt(4, (int) (q.getCompletionTime() / 1000));
ps.setInt(5, q.getForfeited());
ps.executeUpdate();
try (ResultSet rs = ps.getGeneratedKeys()) {
rs.next();
for (int mob : q.getProgress().keySet()) {
pse.setInt(1, rs.getInt(1));
pse.setInt(2, mob);
pse.setString(3, q.getProgress(mob));
pse.addBatch();
synchronized (quests) {
for (MapleQuestStatus q : quests.values()) {
ps.setInt(2, q.getQuest().getId());
ps.setInt(3, q.getStatus().getId());
ps.setInt(4, (int) (q.getCompletionTime() / 1000));
ps.setInt(5, q.getForfeited());
ps.executeUpdate();
try (ResultSet rs = ps.getGeneratedKeys()) {
rs.next();
for (int mob : q.getProgress().keySet()) {
pse.setInt(1, rs.getInt(1));
pse.setInt(2, mob);
pse.setString(3, q.getProgress(mob));
pse.addBatch();
}
for (int i = 0; i < q.getMedalMaps().size(); i++) {
psf.setInt(1, rs.getInt(1));
psf.setInt(2, q.getMedalMaps().get(i));
psf.addBatch();
}
pse.executeBatch();
psf.executeBatch();
}
for (int i = 0; i < q.getMedalMaps().size(); i++) {
psf.setInt(1, rs.getInt(1));
psf.setInt(2, q.getMedalMaps().get(i));
psf.addBatch();
}
pse.executeBatch();
psf.executeBatch();
}
}
}
@@ -5334,7 +5358,9 @@ public class MapleCharacter extends AbstractAnimatedMapleMapObject {
MapleQuestStatus qs = getQuest(q);
qs.setInfo(info);
quests.put(q.getId(), qs);
synchronized (quests) {
quests.put(q.getId(), qs);
}
announce(MaplePacketCreator.updateQuest(qs, false));
if (qs.getQuest().getInfoNumber() > 0) {
@@ -5354,7 +5380,9 @@ public class MapleCharacter extends AbstractAnimatedMapleMapObject {
}
public void updateQuest(MapleQuestStatus quest) {
quests.put(quest.getQuestID(), quest);
synchronized (quests) {
quests.put(quest.getQuestID(), quest);
}
if (quest.getStatus().equals(MapleQuestStatus.Status.STARTED)) {
announce(MaplePacketCreator.updateQuest(quest, false));
if (quest.getQuest().getInfoNumber() > 0) {
@@ -5375,17 +5403,20 @@ public class MapleCharacter extends AbstractAnimatedMapleMapObject {
}
}
public void questTimeLimit(final MapleQuest quest, int time) {
public void questTimeLimit(final MapleQuest quest, int seconds) {
final MapleCharacter chr = this;
ScheduledFuture<?> sf = TimerManager.getInstance().schedule(new Runnable() {
@Override
public void run() {
if(chr.getQuestStatus(quest.getId()) == MapleQuestStatus.Status.COMPLETED.getId()) return;
announce(MaplePacketCreator.questExpire(quest.getId()));
MapleQuestStatus newStatus = new MapleQuestStatus(quest, MapleQuestStatus.Status.NOT_STARTED);
newStatus.setForfeited(getQuest(quest).getForfeited() + 1);
updateQuest(newStatus);
}
}, time * 60 * 1000);
announce(MaplePacketCreator.addQuestTimeLimit(quest.getId(), time * 60 * 1000));
}, seconds * 1000);
announce(MaplePacketCreator.addQuestTimeLimit(quest.getId(), seconds * 1000));
timers.add(sf);
}

View File

@@ -786,7 +786,7 @@ public class MapleClient {
final MapleGuild guild = player.getGuild();
if (channel == -1 || shutdown) {
chrg.setCharacter(null);
if(chrg != null) chrg.setCharacter(null);
removePlayer();
player.saveCooldowns();

View File

@@ -97,18 +97,19 @@ public class MapleMount {
}
private void increaseTiredness() {
if(owner != null) {
this.tiredness++;
owner.getMap().broadcastMessage(MaplePacketCreator.updateMount(owner.getId(), this, false));
if (tiredness > 99) {
this.tiredness = 99;
owner.dispelSkill(owner.getJobType() * 10000000 + 1004);
}
} else {
if(this.tirednessSchedule != null) {
this.tirednessSchedule.cancel(false);
}
}
if(owner != null) {
this.tiredness++;
owner.getMap().broadcastMessage(MaplePacketCreator.updateMount(owner.getId(), this, false));
if (tiredness > 99) {
this.tiredness = 99;
owner.dispelSkill(owner.getJobType() * 10000000 + 1004);
owner.dropMessage("Your mount grew tired! Treat it some revitalizer before riding it again!");
}
} else {
if(this.tirednessSchedule != null) {
this.tirednessSchedule.cancel(false);
}
}
}
public void setExp(int newexp) {

View File

@@ -21,7 +21,6 @@
*/
package net.server.channel.handlers;
import net.server.channel.handlers.AbstractMovementPacketHandler;
import java.util.List;
import client.MapleCharacter;
import client.MapleClient;
@@ -30,6 +29,7 @@ import tools.MaplePacketCreator;
import tools.data.input.SeekableLittleEndianAccessor;
public final class MovePetHandler extends AbstractMovementPacketHandler {
@Override
public final void handlePacket(SeekableLittleEndianAccessor slea, MapleClient c) {
int petId = slea.readInt();
slea.readLong();

View File

@@ -191,6 +191,7 @@ public final class PlayerLoggedinHandler extends AbstractMaplePacketHandler {
} else {
playerGuild.getMGC(player.getId()).setCharacter(player);
player.setMGC(playerGuild.getMGC(player.getId()));
server.setGuildMemberOnline(player, true, c.getChannel());
c.announce(MaplePacketCreator.showGuildInfo(player));
int allianceId = player.getGuild().getAllianceId();
@@ -204,6 +205,7 @@ public final class PlayerLoggedinHandler extends AbstractMaplePacketHandler {
player.getGuild().setAllianceId(0);
}
}
if (newAlliance != null) {
c.announce(MaplePacketCreator.updateAllianceInfo(newAlliance, c));
c.announce(MaplePacketCreator.allianceNotice(newAlliance.getId(), newAlliance.getNotice()));
@@ -267,11 +269,11 @@ public final class PlayerLoggedinHandler extends AbstractMaplePacketHandler {
if (player.getMap().getHPDec() > 0) {
final MapleCharacter mc = player;
ScheduledFuture<?> hpDecreaseTask = TimerManager.getInstance().schedule(new Runnable() {
@Override
public void run() {
mc.doHurtHp();
}
TimerManager.getInstance().schedule(new Runnable() {
@Override
public void run() {
mc.doHurtHp();
}
}, 10000);
}
}

View File

@@ -194,6 +194,8 @@ public class World {
}
public MapleGuild getGuild(MapleGuildCharacter mgc) {
if(mgc == null) return null;
int gid = mgc.getGuildId();
MapleGuild g;
g = Server.getInstance().getGuild(gid, mgc.getWorld(), mgc.getCharacter());

View File

@@ -943,7 +943,10 @@ public class MapleStatEffect {
if (applyto.getMount() == null) {
applyto.mount(ridingLevel, sourceid);
}
applyto.getMount().startSchedule();
if(!(ServerConstants.PETS_NEVER_HUNGRY || applyto.isGM() && ServerConstants.GM_PETS_NEVER_HUNGRY)) {
applyto.getMount().startSchedule();
}
}
if (sourceid == Corsair.BATTLE_SHIP) {
givemount = new MapleMount(applyto, 1932000, sourceid);

View File

@@ -26,6 +26,7 @@ import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import tools.LogHelper;
import tools.MaplePacketCreator;
@@ -33,10 +34,11 @@ import client.MapleCharacter;
import client.inventory.Item;
import client.inventory.MapleInventoryType;
import constants.ItemConstants;
import constants.ServerConstants;
/**
*
* @author Matze
* @author Matze, Ronan (concurrency safety)
*/
public class MapleTrade {
private MapleTrade partner = null;
@@ -44,7 +46,7 @@ public class MapleTrade {
private List<Item> exchangeItems;
private int meso = 0;
private int exchangeMeso;
boolean locked = false;
private AtomicBoolean locked = new AtomicBoolean(false);
private MapleCharacter chr;
private byte number;
private boolean fullTrade = false;
@@ -73,7 +75,7 @@ public class MapleTrade {
}
private void lock() {
locked = true;
locked.set(true);
partner.getChr().getClient().announce(MaplePacketCreator.getTradeConfirmation());
}
@@ -83,16 +85,18 @@ public class MapleTrade {
}
private void complete2() {
boolean show = ServerConstants.USE_DEBUG;
items.clear();
meso = 0;
for (Item item : exchangeItems) {
if ((item.getFlag() & ItemConstants.KARMA) == ItemConstants.KARMA)
item.setFlag((byte) (item.getFlag() ^ ItemConstants.KARMA)); //items with scissors of karma used on them are reset once traded
MapleInventoryManipulator.addFromDrop(chr.getClient(), item, true);
MapleInventoryManipulator.addFromDrop(chr.getClient(), item, show);
}
if (exchangeMeso > 0) {
chr.gainMeso(exchangeMeso - getFee(exchangeMeso), true, true, true);
chr.gainMeso(exchangeMeso - getFee(exchangeMeso), show, true, show);
}
exchangeMeso = 0;
if (exchangeItems != null) {
@@ -102,11 +106,13 @@ public class MapleTrade {
}
private void cancel() {
boolean show = ServerConstants.USE_DEBUG;
for (Item item : items) {
MapleInventoryManipulator.addFromDrop(chr.getClient(), item, true);
MapleInventoryManipulator.addFromDrop(chr.getClient(), item, show);
}
if (meso > 0) {
chr.gainMeso(meso, true, true, true);
chr.gainMeso(meso, show, true, show);
}
meso = 0;
if (items != null) {
@@ -120,7 +126,7 @@ public class MapleTrade {
}
private boolean isLocked() {
return locked;
return locked.get();
}
private int getMeso() {
@@ -128,7 +134,7 @@ public class MapleTrade {
}
public void setMeso(int meso) {
if (locked) {
if (locked.get()) {
throw new RuntimeException("Trade is locked.");
}
if (meso < 0) {
@@ -166,7 +172,7 @@ public class MapleTrade {
}
public void setPartner(MapleTrade partner) {
if (locked) {
if (locked.get()) {
return;
}
this.partner = partner;
@@ -210,9 +216,15 @@ public class MapleTrade {
if (partner.isLocked()) {
local.complete1();
partner.complete1();
if (!local.fitsInInventory() || !partner.fitsInInventory()) {
if (!local.fitsInInventory()) {
cancelTrade(c);
c.message("There is not enough inventory space to complete the trade.");
partner.getChr().message("Partner does not have enough inventory space to complete the trade.");
return;
}
else if (!partner.fitsInInventory()) {
cancelTrade(c);
c.message("Partner does not have enough inventory space to complete the trade.");
partner.getChr().message("There is not enough inventory space to complete the trade.");
return;
}
@@ -224,13 +236,13 @@ public class MapleTrade {
} else {
local.getChr().addMesosTraded(local.exchangeMeso);
}
} else if (c.getTrade().getChr().getLevel() < 15) {
if (c.getMesosTraded() + c.getTrade().exchangeMeso > 1000000) {
} else if (partner.getChr().getLevel() < 15) {
if (partner.getChr().getMesosTraded() + partner.exchangeMeso > 1000000) {
cancelTrade(c);
c.getClient().announce(MaplePacketCreator.serverNotice(1, "Characters under level 15 may not trade more than 1 million mesos per day."));
partner.getChr().getClient().announce(MaplePacketCreator.serverNotice(1, "Characters under level 15 may not trade more than 1 million mesos per day."));
return;
} else {
c.addMesosTraded(local.exchangeMeso);
partner.getChr().addMesosTraded(partner.exchangeMeso);
}
}
LogHelper.logTrade(local, partner);

View File

@@ -46,7 +46,7 @@ public class MapleQuest {
private static Map<Integer, MapleQuest> quests = new HashMap<>();
protected short infoNumber, id;
protected int timeLimit, timeLimit2;
protected int timeLimit;
protected String infoex;
protected Map<MapleQuestRequirementType, MapleQuestRequirement> startReqs = new EnumMap<>(MapleQuestRequirementType.class);
protected Map<MapleQuestRequirementType, MapleQuestRequirement> completeReqs = new EnumMap<>(MapleQuestRequirementType.class);
@@ -57,20 +57,22 @@ public class MapleQuest {
private boolean autoPreComplete, autoComplete;
private boolean repeatable = false;
private final static MapleDataProvider questData = MapleDataProviderFactory.getDataProvider(new File(System.getProperty("wzpath") + "/Quest.wz"));
private static MapleData questInfo;
private static MapleData questAct;
private static MapleData questReq;
private static MapleData questInfo;
private static MapleData questAct;
private static MapleData questReq;
private MapleQuest(int id) {
this.id = (short) id;
if(questInfo != null) {
timeLimit = MapleDataTool.getInt("timeLimit", questInfo, 0);
timeLimit2 = MapleDataTool.getInt("timeLimit2", questInfo, 0);
autoStart = MapleDataTool.getInt("autoStart", questInfo, 0) == 1;
autoPreComplete = MapleDataTool.getInt("autoPreComplete", questInfo, 0) == 1;
autoComplete = MapleDataTool.getInt("autoComplete", questInfo, 0) == 1;
}
if(questInfo != null) {
MapleData reqData = questInfo.getChildByPath(String.valueOf(id));
timeLimit = MapleDataTool.getInt("timeLimit", reqData, 0);
timeLimit = Math.max(timeLimit, MapleDataTool.getInt("timeLimit2", reqData, 0)); // alas, nexon made we deal with 2 timeLimits
autoStart = MapleDataTool.getInt("autoStart", reqData, 0) == 1;
autoPreComplete = MapleDataTool.getInt("autoPreComplete", reqData, 0) == 1;
autoComplete = MapleDataTool.getInt("autoComplete", reqData, 0) == 1;
}
MapleData reqData = questReq.getChildByPath(String.valueOf(id));
if (reqData == null) {//most likely infoEx
@@ -84,7 +86,7 @@ public class MapleQuest {
repeatable = true;
}
if (type.equals(MapleQuestRequirementType.INFO_NUMBER)) {
if (type.equals(MapleQuestRequirementType.INFO_NUMBER)) {
infoNumber = (short) MapleDataTool.getInt(startReq, 0);
}
@@ -280,11 +282,9 @@ public class MapleQuest {
newStatus.setForfeited(c.getQuest(this).getForfeited());
if (timeLimit > 0) {
c.questTimeLimit(this, 30000);//timeLimit * 1000
}
if (timeLimit2 > 0) {//=\
c.questTimeLimit(this, timeLimit);
}
c.updateQuest(newStatus);
return true;
}

View File

@@ -2337,16 +2337,18 @@ public class MaplePacketCreator {
mplew.write(chr.getMarriageRing() != null ? 1 : 0);
String guildName = "";
String allianceName = "";
MapleGuildSummary gs = chr.getClient().getWorldServer().getGuildSummary(chr.getGuildId(), chr.getWorld());
if (chr.getGuildId() > 0 && gs != null) {
guildName = gs.getName();
MapleAlliance alliance = Server.getInstance().getAlliance(gs.getAllianceId());
if (chr.getGuildId() > 0) {
MapleGuild mg = Server.getInstance().getGuild(chr.getGuildId());
guildName = mg.getName();
MapleAlliance alliance = Server.getInstance().getAlliance(chr.getGuild().getAllianceId());
if (alliance != null) {
allianceName = alliance.getName();
}
}
mplew.writeMapleAsciiString(guildName);
mplew.writeMapleAsciiString(allianceName);
mplew.writeMapleAsciiString(allianceName); // does not seems to work
mplew.write(0);
MaplePet[] pets = chr.getPets();
Item inv = chr.getInventory(MapleInventoryType.EQUIPPED).getItem((short) -114);