Files
Server/src/engine/objects/RuneBase.java
T
MagicBotandClaude Opus 5.5 0536aea27a Build rune requirements from the runes and check them like the client
RuneBase(RuneTemplate) fills the race, class and discipline maps from
item_race_req / item_class_req / item_disc_req (names to rune ids through
CObjectIndex; class runes with rune_is_standard_character_creation go to
baseClass, the rest to promotionClass) with a restrict flag for each, and
the level from item_level_req. static_rune_runereq is no longer used for
cached rune bases; GET_RUNE_REQS stays until 2.11.

ApplyRuneMsg follows ArcCharacter_MeetsItemReqs (0x475F40) and
ArcItemRestrict_Check (0x4BAC60): restrict true = blacklist, false =
whitelist. The class check passes when the base class or the promotion is
listed. The discipline check is against the discs already held, using the
new rune's list only.

Parity (offline, scripts/p25a.py with feasible race/promotion pairs under
the client rule; scripts/p25b.py over all 315 rune bases). Value changes,
all decided JSON (D6, 2026-10-21):
- 3001 Animator: + Irekei (4 pairs); Summoner blocks it
- 3008 Commander: Saboteur blocks it
- 3010 Duelist: level 20, 5 races, 7 promotions (had no DB rows; -111 pairs)
- 3018 Enchanter, 3030 Conjurer: + Vampire (4 pairs each)
- 3020 Summoner: - Thief/Warrior/Barbarian (28 pairs), + Centaur Druid/Priest
  (4); Animator blocks it
- 3031 Darksworn: + Irekei Priest (2); 3032 Valkyr: + Human/Aelfborn Warrior (2)
- 3037 Artillerist: + Sentinel (11); 3044 Strigoi: Belgosch blocks it
- 252127 Master of Throwing: - Scout (16 pairs)
- 70 more runes with no DB rows gain their JSON lists (class runes 2500-2526
  except the Pet rune, creation talents and Bloodline talents); only runestones are checked
- Stat chains: the DB "higher rank held" blocks leave the discipline map.
  Ranks 1-8 are still refused by the sub type rule; a rank-0 rune now
  replaces a higher rank as the client does (decided: follow the client)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 10:02:10 -04:00

354 lines
11 KiB
Java

// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ .
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌·
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌
// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀
// Magicbane Emulator Project © 2013 - 2026
// https://www.magicbane.com
package engine.objects;
import engine.gameManager.DbManager;
import engine.mbEnums;
import engine.net.ByteBufferWriter;
import engine.parsers.cache.CObjectIndex;
import engine.parsers.objects.RuneTemplate;
import engine.server.MBServerStatics;
import engine.util.Hasher;
import org.pmw.tinylog.Logger;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.concurrent.ConcurrentHashMap;
public class RuneBase extends AbstractGameObject {
public static HashMap<Integer, ArrayList<Integer>> AllowedBaseClassRunesMap = new HashMap<>();
public static HashMap<Integer, ArrayList<Integer>> AllowedRaceRunesMap = new HashMap<>();
private final String name;
private final String description;
private final int type;
private final byte subtype;
private final ConcurrentHashMap<Integer, Boolean> race = new ConcurrentHashMap<>(MBServerStatics.CHM_INIT_CAP, MBServerStatics.CHM_LOAD, MBServerStatics.CHM_THREAD_LOW);
private final ConcurrentHashMap<Integer, Boolean> baseClass = new ConcurrentHashMap<>(MBServerStatics.CHM_INIT_CAP, MBServerStatics.CHM_LOAD, MBServerStatics.CHM_THREAD_LOW);
private final ConcurrentHashMap<Integer, Boolean> promotionClass = new ConcurrentHashMap<>(MBServerStatics.CHM_INIT_CAP, MBServerStatics.CHM_LOAD, MBServerStatics.CHM_THREAD_LOW);
private final ConcurrentHashMap<Integer, Boolean> discipline = new ConcurrentHashMap<>(MBServerStatics.CHM_INIT_CAP, MBServerStatics.CHM_LOAD, MBServerStatics.CHM_THREAD_LOW);
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;
private boolean raceRestrict;
private boolean classRestrict;
private boolean discRestrict;
/**
* No Table ID Constructor
*/
public RuneBase(String name, String description, int type, byte subtype, ArrayList<RuneBaseAttribute> attrs) {
super();
this.name = name;
this.description = description;
this.type = type;
this.subtype = subtype;
}
/**
* Normal Constructor
*/
public RuneBase(String name, String description, int type, byte subtype, ArrayList<RuneBaseAttribute> attrs, int newUUID) {
super(newUUID);
this.name = name;
this.description = description;
this.type = type;
this.subtype = subtype;
}
/**
* ResultSet Constructor
*/
public RuneBase(ResultSet rs) throws SQLException {
super(rs);
this.name = rs.getString("name");
this.description = rs.getString("description");
this.type = rs.getInt("type");
this.subtype = rs.getByte("subtype");
// static_rune_runereq race/class rows are whitelists, its discipline rows prohibitions
this.discRestrict = true;
DbManager.RuneBaseQueries.GET_RUNE_REQS(this);
this.effectsList = DbManager.MobBaseQueries.GET_RUNEBASE_EFFECTS(this.getObjectUUID());
}
/**
* 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 effects until 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);
Integer classRuneID;
Integer discRuneID;
ArrayList<Integer> raceRuneIDs;
RuneTemplate classRune;
this.name = rune.item_base_name;
this.description = rune.rune_dsc;
this.type = dbRuneBase.type;
this.subtype = dbRuneBase.subtype;
this.effectsList = dbRuneBase.effectsList;
this.runeType = rune.rune_type;
this.rank = rune.rune_rank;
this.levelRequired = rune.item_level_req;
this.raceRestrict = rune.item_race_req_restrict;
this.classRestrict = rune.item_class_req_restrict;
this.discRestrict = rune.item_disc_req_restrict;
for (mbEnums.MonsterType raceType : rune.item_race_req) {
raceRuneIDs = CObjectIndex.getRaceRuneIds(raceType);
if (raceRuneIDs.isEmpty())
Logger.warn("RuneBase " + rune.template_id + ": race " + raceType + " has no race rune");
for (int raceRuneID : raceRuneIDs)
this.race.put(raceRuneID, true);
}
for (mbEnums.ClassType classType : rune.item_class_req) {
classRuneID = CObjectIndex.getClassRuneId(classType);
if (classRuneID == null) {
Logger.warn("RuneBase " + rune.template_id + ": class " + classType + " has no class rune");
continue;
}
classRune = RuneTemplate._runes.get(classRuneID);
if (classRune.rune_is_standard_character_creation)
this.baseClass.put(classRuneID, true);
else
this.promotionClass.put(classRuneID, true);
}
for (mbEnums.DisciplineType discType : rune.item_disc_req) {
discRuneID = CObjectIndex.getDisciplineRuneId(discType);
if (discRuneID == null) {
Logger.warn("RuneBase " + rune.template_id + ": discipline " + discType + " has no discipline rune");
continue;
}
this.discipline.put(discRuneID, true);
}
// 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) {
if (tableId == 0)
return null;
RuneBase rb = (RuneBase) DbManager.getFromCache(mbEnums.GameObjectType.RuneBase, tableId);
if (rb != null)
return rb;
return DbManager.RuneBaseQueries.GET_RUNEBASE(tableId);
}
public static void serializeForClientMsg(RuneBase runeBase, ByteBufferWriter writer) {
writer.putInt(runeBase.type);
writer.putInt(0); // Pad
writer.putInt(runeBase.getObjectUUID());
writer.putInt(runeBase.getObjectType().ordinal());
writer.putInt(runeBase.getObjectUUID());
}
// Iterates the static_rune_runebase rows: no rune field separates the player runes from the mob
// talents, and each row still holds the server type/subtype.
public static void LoadAllRuneBases() {
ArrayList<RuneBase> dbRuneBases;
ArrayList<RuneBaseAttribute> attributes;
RuneTemplate rune;
int loadedCount = 0;
dbRuneBases = DbManager.RuneBaseQueries.LOAD_ALL_RUNEBASES();
RuneBaseAttribute.runeBaseAttributeMap.clear();
for (RuneBase dbRuneBase : dbRuneBases) {
rune = RuneTemplate._runes.get(dbRuneBase.getObjectUUID());
if (rune == null) {
Logger.warn("RuneBase " + dbRuneBase.getObjectUUID() + " has no rune; loaded from static_rune_runebase without attributes");
continue;
}
DbManager.addToCache(new RuneBase(rune, dbRuneBase));
attributes = RuneBaseAttribute.getAttributesForRune(rune);
// getAttrs() returns null for a rune without attributes, as the DB map did
if (!attributes.isEmpty())
RuneBaseAttribute.runeBaseAttributeMap.put(rune.template_id, attributes);
loadedCount++;
}
Logger.info("Loaded " + loadedCount + " rune bases from runes");
RuneBase.AllowedBaseClassRunesMap = DbManager.RuneBaseQueries.LOAD_ALLOWED_STARTING_RUNES_FOR_BASECLASS();
RuneBase.AllowedRaceRunesMap = DbManager.RuneBaseQueries.LOAD_ALLOWED_STARTING_RUNES_FOR_RACE();
}
@Override
public boolean equals(Object obj) {
if (!super.equals(obj)) {
return false;
}
if (obj instanceof RuneBase) {
RuneBase rbObj = (RuneBase) obj;
if (!this.name.equals(rbObj.name)) {
return false;
}
if (!this.description.equals(rbObj.description)) {
return false;
}
if (this.type != rbObj.type) {
return false;
}
if (this.subtype != rbObj.subtype) {
return false;
}
return true;
}
return false;
}
/*
* Getters
*/
public String getName() {
return name;
}
public String getDescription() {
return description;
}
public int getType() {
return type;
}
/**
* @return the subtype
*/
public byte getSubtype() {
return subtype;
}
/**
* @return the attrs
*/
public ArrayList<RuneBaseAttribute> getAttrs() {
return RuneBaseAttribute.runeBaseAttributeMap.get(this.getObjectUUID());
}
public ConcurrentHashMap<Integer, Boolean> getRace() {
return this.race;
}
public ConcurrentHashMap<Integer, Boolean> getBaseClass() {
return this.baseClass;
}
public ConcurrentHashMap<Integer, Boolean> getPromotionClass() {
return this.promotionClass;
}
public ConcurrentHashMap<Integer, Boolean> getDiscipline() {
return this.discipline;
}
public ArrayList<Integer> getOverwrite() {
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;
}
// restrict true: the list is a blacklist; false: a whitelist
public boolean isRaceRestrict() {
return this.raceRestrict;
}
public boolean isClassRestrict() {
return this.classRestrict;
}
public boolean isDiscRestrict() {
return this.discRestrict;
}
/*
* Serializing
*/
public int getLevelRequired() {
return this.levelRequired;
}
public void setLevelRequired(int levelRequired) {
this.levelRequired = levelRequired;
}
@Override
public void updateDatabase() {
// TODO Auto-generated method stub
}
/**
* @return the effectsList
*/
public ArrayList<MobBaseEffects> getEffectsList() {
return effectsList;
}
}