Replace talents by sub type and rank, cap talents at 8

Follows the client's ArcCharacter_AddRune (0x4695D0, specs/RuneRequirements.md):
a talent replaces a held talent with the same non-zero rune_sub_type when its
rank is 0 or higher than the held rank; otherwise the apply fails. With no
talent to replace, a new talent needs fewer than 8 held talents.

RuneBase gets runeType, a sub type token (SBStringHash(rune_sub_type), or the
stored hash) and rank from the rune. It no longer copies the static_rune_runereq
type 5 overwrite list, and ApplyRuneMsg no longer reads it.

Value changes:
- Stat chains (*MaxIncrease) replace the same ranks as before; the DB typos go
  away (250026 listed 250019 twice and missed 250018)
- Talents in the same group now replace each other when applied from a
  runestone (e.g. Bloodline sub-race talents); before, both were added
- Limit: 8 talents instead of more than 12 runes of any kind
- Rank-0 downgrade (decided: follow the client) takes effect in the next
  commit, when the DB stat-chain blocks leave the discipline map

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
MagicBotandClaude Opus 5.5 committed 2026-10-21 10:00:46 -04:00
1 parent 8f87778c43
commit c3f1861cf7
2 files changed
+57 -12

No files matched your search

+31 -11
View File
@@ -26,6 +26,9 @@ import java.util.concurrent.ConcurrentHashMap;
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;
@@ -158,6 +161,31 @@ public class ApplyRuneMsg extends ClientNetMsg {
return false;
}
// Client 0x4695D0: a talent replaces a held one with the same sub type when its rank is 0 or higher
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() != 0 && rb.getRank() <= replacedRune.getRuneBase().getRank())
return false;
} else if (talentCount >= MAX_TALENTS)
return false;
}
int strTotal = 0;
int dexTotal = 0;
int conTotal = 0;
@@ -227,11 +255,6 @@ public class ApplyRuneMsg extends ClientNetMsg {
}
}
//Check if max number runes already reached
if (runes.size() > 12) {
return false;
}
//if discipline, check number applied
if (isDiscipline(runeID)) {
if (playerCharacter.getLevel() < 70) {
@@ -259,13 +282,10 @@ public class ApplyRuneMsg extends ClientNetMsg {
return false;
}
//remove any overridden runes from player
ArrayList<Integer> overwrite = rb.getOverwrite();
//remove the replaced talent from player
CharacterRune toRemove = null;
if (overwrite.size() > 0) {
for (int overwriteID : overwrite) {
toRemove = playerCharacter.removeRune(overwriteID);
}
if (replacedRune != null) {
toRemove = playerCharacter.removeRune(replacedRune.getRuneBaseID());
}
//add rune to player
+26 -1
View File
@@ -14,6 +14,7 @@ import engine.mbEnums;
import engine.net.ByteBufferWriter;
import engine.parsers.objects.RuneTemplate;
import engine.server.MBServerStatics;
import engine.util.Hasher;
import org.pmw.tinylog.Logger;
import java.sql.ResultSet;
@@ -38,6 +39,9 @@ public class RuneBase extends AbstractGameObject {
private final ArrayList<Integer> overwrite = new ArrayList<>();
private int levelRequired = 1;
private ArrayList<MobBaseEffects> effectsList = new ArrayList<>();
private mbEnums.RuneType runeType;
private int subTypeToken;
private int rank;
/**
* No Table ID Constructor
@@ -84,6 +88,7 @@ public class RuneBase extends AbstractGameObject {
* Rune Template Constructor
* type and subtype are server enums that don't map cleanly from rune_type/rune_sub_type, so they
* come from the static_rune_runebase row, as do the requirement maps and effects until 2.5b/2.10.
* Talents with the same sub type token replace each other by rank (client 0x4695D0), so no overwrite list.
*/
public RuneBase(RuneTemplate rune, RuneBase dbRuneBase) {
super(rune.template_id);
@@ -96,9 +101,16 @@ public class RuneBase extends AbstractGameObject {
this.baseClass.putAll(dbRuneBase.baseClass);
this.promotionClass.putAll(dbRuneBase.promotionClass);
this.discipline.putAll(dbRuneBase.discipline);
this.overwrite.addAll(dbRuneBase.overwrite);
this.levelRequired = dbRuneBase.levelRequired;
this.effectsList = dbRuneBase.effectsList;
this.runeType = rune.rune_type;
this.rank = rune.rune_rank;
// Same token as CObjectIndex: unresolved sub types are stored as their hash
if (!rune.rune_sub_type.isEmpty())
this.subTypeToken = Hasher.SBStringHash(rune.rune_sub_type);
else
this.subTypeToken = rune.rune_sub_type_hash;
}
public static RuneBase getRuneBase(int tableId) {
@@ -241,6 +253,19 @@ public class RuneBase extends AbstractGameObject {
return this.overwrite;
}
public mbEnums.RuneType getRuneType() {
return this.runeType;
}
// 0 when the rune has no sub type; talents sharing a non-zero token replace each other
public int getSubTypeToken() {
return this.subTypeToken;
}
public int getRank() {
return this.rank;
}
/*
* Serializing
*/