Test on dev (character Moo): with Enhanced Strength held, the client shaded an Increased Strength runestone red and showed its refusal popup, but the server applied it as a downgrade. The client's 0x4768D0 (ArcRuneTemplate vtable slot 0x1E8) blocks a talent when a held talent with the same rune_sub_type has an equal or higher rank, rank 0 included. AddRune's rank-0 replace branch is never reached past that check. Value changes vs the previous commit: - Rank-0 stat runes are refused while any rank of their chain is held (same as the old DB type 4 block) - Rank-0 talents in the same group (e.g. Bloodline sub-race runestones) are refused instead of replacing each other Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
395 lines
14 KiB
Java
395 lines
14 KiB
Java
// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ .
|
|
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌·
|
|
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀
|
|
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌
|
|
// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀
|
|
// Magicbane Emulator Project © 2013 - 2026
|
|
// https://www.magicbane.com
|
|
|
|
|
|
package engine.net.client.msg;
|
|
|
|
import engine.gameManager.DbManager;
|
|
import engine.gameManager.DispatchManager;
|
|
import engine.mbEnums;
|
|
import engine.net.*;
|
|
import engine.net.client.ClientConnection;
|
|
import engine.objects.CharacterRune;
|
|
import engine.objects.PlayerCharacter;
|
|
import engine.objects.RuneBase;
|
|
import engine.objects.RuneBaseAttribute;
|
|
import engine.server.MBServerStatics;
|
|
import org.pmw.tinylog.Logger;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.Arrays;
|
|
import java.util.Collection;
|
|
import java.util.Collections;
|
|
import java.util.HashSet;
|
|
import java.util.Set;
|
|
|
|
public class ApplyRuneMsg extends ClientNetMsg {
|
|
|
|
// Client ArcCharacter_AddRune (0x4695D0)
|
|
private static final int MAX_TALENTS = 8;
|
|
|
|
private int targetType;
|
|
private int targetID;
|
|
private int removeRuneBase;
|
|
private int runeBase;
|
|
private int runeType;
|
|
private int runeID;
|
|
private Boolean isPromo;
|
|
|
|
/**
|
|
* This is the general purpose constructor.
|
|
*/
|
|
public ApplyRuneMsg(int targetType, int targetID, int runeBase, int runeType, int runeID, Boolean isPromo) {
|
|
super(Protocol.SETRUNE);
|
|
this.targetType = targetType;
|
|
this.targetID = targetID;
|
|
this.runeBase = runeBase;
|
|
this.runeType = runeType;
|
|
this.runeID = runeID;
|
|
this.isPromo = isPromo;
|
|
this.removeRuneBase = 0;
|
|
}
|
|
|
|
public ApplyRuneMsg(int targetType, int targetID, int removeRuneBase) {
|
|
super(Protocol.SETRUNE);
|
|
this.targetType = targetType;
|
|
this.targetID = targetID;
|
|
this.runeBase = 0;
|
|
this.runeType = 0;
|
|
this.runeID = 0;
|
|
this.isPromo = false;
|
|
this.removeRuneBase = removeRuneBase;
|
|
}
|
|
|
|
/**
|
|
* This constructor is used by NetMsgFactory. It attempts to deserialize the
|
|
* ByteBuffer into a message. If a BufferUnderflow occurs (based on reading
|
|
* past the limit) then this constructor Throws that Exception to the
|
|
* caller.
|
|
*/
|
|
public ApplyRuneMsg(AbstractConnection origin, ByteBufferReader reader) {
|
|
super(Protocol.SETRUNE, origin, reader);
|
|
}
|
|
|
|
public static boolean applyRune(int runeID, ClientConnection origin, PlayerCharacter playerCharacter) {
|
|
|
|
RuneBase rb = RuneBase.getRuneBase(runeID);
|
|
Dispatch dispatch;
|
|
|
|
if (playerCharacter == null || origin == null || rb == null) {
|
|
return false;
|
|
}
|
|
|
|
ArrayList<CharacterRune> runes = playerCharacter.getRunes();
|
|
HashSet<Integer> classIDs = new HashSet<>();
|
|
HashSet<Integer> heldDiscIDs = new HashSet<>();
|
|
|
|
// Client ArcCharacter_MeetsItemReqs (0x475F40)
|
|
if (!meetsListReq(rb.getRace().keySet(), rb.isRaceRestrict(), Collections.singleton(playerCharacter.getRaceID())))
|
|
return false;
|
|
|
|
// A listed base class covers its promotions: base or promotion listed is enough
|
|
classIDs.addAll(rb.getBaseClass().keySet());
|
|
classIDs.addAll(rb.getPromotionClass().keySet());
|
|
|
|
if (!meetsListReq(classIDs, rb.isClassRestrict(), Arrays.asList(playerCharacter.getBaseClassID(), playerCharacter.getPromotionClassID())))
|
|
return false;
|
|
|
|
// Only the new rune's list is checked, not the held discs' lists
|
|
for (CharacterRune cr : runes)
|
|
if (isDiscipline(cr.getRuneBaseID()))
|
|
heldDiscIDs.add(cr.getRuneBaseID());
|
|
|
|
if (!meetsListReq(rb.getDiscipline().keySet(), rb.isDiscRestrict(), heldDiscIDs))
|
|
return false;
|
|
|
|
int discCount = 0;
|
|
for (CharacterRune cr : runes) {
|
|
int runeBaseID = cr.getRuneBaseID();
|
|
//count number of discipline runes
|
|
if (runeBaseID > 3000 && runeBaseID < 3049) {
|
|
discCount++;
|
|
}
|
|
//see if rune is already applied
|
|
if (runeBaseID == runeID) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
//Check level is met
|
|
if (playerCharacter.getLevel() < rb.getLevelRequired()) {
|
|
return false;
|
|
}
|
|
|
|
// Client 0x4768D0 refuses a talent unless its rank beats the held one with the same sub type (rank 0 included);
|
|
// AddRune (0x4695D0) then replaces the held talent
|
|
CharacterRune replacedRune = null;
|
|
|
|
if (rb.getRuneType() == mbEnums.RuneType.TALENT) {
|
|
int talentCount = 0;
|
|
|
|
for (CharacterRune cr : runes) {
|
|
RuneBase heldRuneBase = cr.getRuneBase();
|
|
|
|
if (heldRuneBase == null || heldRuneBase.getRuneType() != mbEnums.RuneType.TALENT)
|
|
continue;
|
|
|
|
talentCount++;
|
|
|
|
if (rb.getSubTypeToken() != 0 && heldRuneBase.getSubTypeToken() == rb.getSubTypeToken())
|
|
replacedRune = cr;
|
|
}
|
|
|
|
if (replacedRune != null) {
|
|
if (rb.getRank() <= replacedRune.getRuneBase().getRank())
|
|
return false;
|
|
} else if (talentCount >= MAX_TALENTS)
|
|
return false;
|
|
}
|
|
|
|
int strTotal = 0;
|
|
int dexTotal = 0;
|
|
int conTotal = 0;
|
|
int intTotal = 0;
|
|
int spiTotal = 0;
|
|
int cost = 0;
|
|
|
|
//Check any attributes are met
|
|
ArrayList<RuneBaseAttribute> attrs = rb.getAttrs();
|
|
|
|
if (rb.getAttrs() != null)
|
|
for (RuneBaseAttribute rba : attrs) {
|
|
int attrID = rba.getAttributeID();
|
|
int mod = rba.getModValue();
|
|
switch (attrID) {
|
|
case MBServerStatics.RUNE_COST_ATTRIBUTE_ID:
|
|
if (mod > playerCharacter.getUnusedStatPoints()) {
|
|
return false;
|
|
}
|
|
cost = mod;
|
|
break;
|
|
case MBServerStatics.RUNE_STR_MIN_NEEDED_ATTRIBUTE_ID:
|
|
if ((int) playerCharacter.statStrBase < mod) {
|
|
return false;
|
|
}
|
|
strTotal = mod;
|
|
break;
|
|
case MBServerStatics.RUNE_DEX_MIN_NEEDED_ATTRIBUTE_ID:
|
|
if ((int) playerCharacter.statDexBase < mod) {
|
|
return false;
|
|
}
|
|
dexTotal = mod;
|
|
break;
|
|
case MBServerStatics.RUNE_CON_MIN_NEEDED_ATTRIBUTE_ID:
|
|
if ((int) playerCharacter.statConBase < mod) {
|
|
return false;
|
|
}
|
|
conTotal = mod;
|
|
break;
|
|
case MBServerStatics.RUNE_INT_MIN_NEEDED_ATTRIBUTE_ID:
|
|
if ((int) playerCharacter.statIntBase < mod) {
|
|
return false;
|
|
}
|
|
intTotal = mod;
|
|
break;
|
|
case MBServerStatics.RUNE_SPI_MIN_NEEDED_ATTRIBUTE_ID:
|
|
if ((int) playerCharacter.statSpiBase < mod) {
|
|
return false;
|
|
}
|
|
spiTotal = mod;
|
|
break;
|
|
case MBServerStatics.RUNE_STR_ATTRIBUTE_ID:
|
|
strTotal += mod;
|
|
break;
|
|
case MBServerStatics.RUNE_DEX_ATTRIBUTE_ID:
|
|
dexTotal += mod;
|
|
break;
|
|
case MBServerStatics.RUNE_CON_ATTRIBUTE_ID:
|
|
conTotal += mod;
|
|
break;
|
|
case MBServerStatics.RUNE_INT_ATTRIBUTE_ID:
|
|
intTotal += mod;
|
|
break;
|
|
case MBServerStatics.RUNE_SPI_ATTRIBUTE_ID:
|
|
spiTotal += mod;
|
|
break;
|
|
}
|
|
}
|
|
|
|
//if discipline, check number applied
|
|
if (isDiscipline(runeID)) {
|
|
if (playerCharacter.getLevel() < 70) {
|
|
if (discCount > 2) {
|
|
return false;
|
|
}
|
|
} else {
|
|
if (discCount > 3) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
//Everything succeeded. Let's apply the rune
|
|
//Attempt add rune to database
|
|
CharacterRune runeWithoutID = new CharacterRune(rb, playerCharacter.getObjectUUID());
|
|
CharacterRune cr;
|
|
try {
|
|
cr = DbManager.CharacterRuneQueries.ADD_CHARACTER_RUNE(runeWithoutID);
|
|
} catch (Exception e) {
|
|
cr = null;
|
|
Logger.error(e);
|
|
}
|
|
if (cr == null) {
|
|
return false;
|
|
}
|
|
|
|
//remove the replaced talent from player
|
|
CharacterRune toRemove = null;
|
|
if (replacedRune != null) {
|
|
toRemove = playerCharacter.removeRune(replacedRune.getRuneBaseID());
|
|
}
|
|
|
|
//add rune to player
|
|
playerCharacter.addRune(cr);
|
|
|
|
// recalculate all bonuses/formulas/skills/powers
|
|
playerCharacter.recalculate();
|
|
|
|
//if overwriting a stat rune, add any amount granted from previous rune.
|
|
if (toRemove != null) {
|
|
RuneBase rbs = toRemove.getRuneBase();
|
|
if (rbs != null && rbs.getObjectUUID() > 249999 && rbs.getObjectUUID() < 250045) {
|
|
//add any additional stats to match old amount
|
|
int dif = strTotal - (int) playerCharacter.statStrBase;
|
|
if (dif > 0 && strTotal < (int) playerCharacter.statStrMax) {
|
|
playerCharacter.addStr(dif);
|
|
}
|
|
dif = dexTotal - (int) playerCharacter.statDexBase;
|
|
if (dif > 0 && dexTotal < (int) playerCharacter.statDexMax) {
|
|
playerCharacter.addDex(dif);
|
|
}
|
|
dif = conTotal - (int) playerCharacter.statConBase;
|
|
if (dif > 0 && conTotal < (int) playerCharacter.statConMax) {
|
|
playerCharacter.addCon(dif);
|
|
}
|
|
dif = intTotal - (int) playerCharacter.statIntBase;
|
|
if (dif > 0 && intTotal < (int) playerCharacter.statIntMax) {
|
|
playerCharacter.addInt(dif);
|
|
}
|
|
dif = spiTotal - (int) playerCharacter.statSpiBase;
|
|
if (dif > 0 && spiTotal < (int) playerCharacter.statSpiMax) {
|
|
playerCharacter.addSpi(dif);
|
|
}
|
|
|
|
// recalculate all bonuses/formulas/skills/powers
|
|
playerCharacter.recalculate();
|
|
}
|
|
}
|
|
|
|
if (cost > 0) {
|
|
ModifyStatMsg msm = new ModifyStatMsg((0 - cost), 0, 3);
|
|
dispatch = Dispatch.borrow(playerCharacter, msm);
|
|
DispatchManager.dispatchMsgDispatch(dispatch, mbEnums.DispatchChannel.SECONDARY);
|
|
}
|
|
|
|
//send apply rune message to client
|
|
ApplyRuneMsg arm = new ApplyRuneMsg(playerCharacter.getObjectType().ordinal(), playerCharacter.getObjectUUID(), runeID, cr.getObjectType().ordinal(), cr.getObjectUUID(), false);
|
|
dispatch = Dispatch.borrow(playerCharacter, arm);
|
|
DispatchManager.dispatchMsgDispatch(dispatch, mbEnums.DispatchChannel.SECONDARY);
|
|
|
|
|
|
//alert them of success
|
|
ErrorPopupMsg.sendErrorPopup(playerCharacter, 160);
|
|
|
|
//reapply bonuses
|
|
playerCharacter.applyBonuses();
|
|
|
|
return true;
|
|
}
|
|
|
|
public static boolean isDiscipline(int runeID) {
|
|
|
|
return runeID > 3000 && runeID < 3050;
|
|
}
|
|
|
|
// Client ArcItemRestrict_Check (0x4BAC60). An empty list has no requirement; no rune has an empty whitelist.
|
|
private static boolean meetsListReq(Set<Integer> listedIDs, boolean restrict, Collection<Integer> heldIDs) {
|
|
|
|
boolean listed = false;
|
|
|
|
if (listedIDs.isEmpty())
|
|
return true;
|
|
|
|
for (int heldID : heldIDs)
|
|
if (listedIDs.contains(heldID))
|
|
listed = true;
|
|
|
|
return listed != restrict;
|
|
}
|
|
|
|
/**
|
|
* Serializes the subclass specific items to the supplied NetMsgWriter.
|
|
*/
|
|
@Override
|
|
protected void _serialize(ByteBufferWriter writer) {
|
|
writer.putInt(this.targetType);
|
|
writer.putInt(this.targetID);
|
|
writer.putInt(0);
|
|
writer.putInt(this.removeRuneBase);
|
|
writer.putInt(0);
|
|
writer.putInt(this.runeBase);
|
|
writer.putInt(this.runeType);
|
|
writer.putInt(this.runeID);
|
|
if (this.isPromo) {
|
|
writer.put((byte) 1);
|
|
} else {
|
|
writer.put((byte) 0);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Deserializes the subclass specific items from the supplied NetMsgReader.
|
|
*/
|
|
@Override
|
|
protected void _deserialize(ByteBufferReader reader) {
|
|
this.targetType = reader.getInt();
|
|
this.targetID = reader.getInt();
|
|
reader.getInt();
|
|
this.removeRuneBase = reader.getInt();
|
|
reader.getInt();
|
|
this.runeBase = reader.getInt();
|
|
this.runeType = reader.getInt();
|
|
this.runeID = reader.getInt();
|
|
this.isPromo = (reader.get() == 1) ? true : false;
|
|
}
|
|
|
|
public int getTargetType() {
|
|
return targetType;
|
|
}
|
|
|
|
public void setTargetType(int value) {
|
|
this.targetType = value;
|
|
}
|
|
|
|
public int getTargetID() {
|
|
return targetID;
|
|
}
|
|
|
|
public void setTargetID(int value) {
|
|
this.targetID = value;
|
|
}
|
|
|
|
public int getRuneID() {
|
|
return runeID;
|
|
}
|
|
|
|
public void setRuneID(int value) {
|
|
this.runeID = value;
|
|
}
|
|
}
|