// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ . // ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌· // ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀ // ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌ // ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀ // Magicbane Emulator Project © 2013 - 2026 // https://www.magicbane.com package engine.objects; import engine.gameManager.DbManager; import engine.mbEnums; import engine.mbEnums.RaceType; import engine.net.ByteBufferWriter; import engine.parsers.cache.CacheParserUtil; 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.HashSet; import java.util.concurrent.ConcurrentHashMap; public class Race { // Local class cache private static ConcurrentHashMap _raceByID = new ConcurrentHashMap<>(MBServerStatics.CHM_INIT_CAP, MBServerStatics.CHM_LOAD, MBServerStatics.CHM_THREAD_LOW); // Creation points before the race rune's rune_creation_cost is taken off private static final int STARTING_POINT_POOL = 30; private final String name; private final String description; private final int raceRuneID; private final short strStart; private final short strMin; private final short strMax; private final short dexStart; private final short dexMin; private final short dexMax; private final short conStart; private final short conMin; private final short conMax; private final short intStart; private final short intMin; private final short intMax; private final short spiStart; private final short spiMin; private final short spiMax; private final short healthBonus; private final short manaBonus; private final short staminaBonus; private final byte startingPoints; private final float minHeight; private final float strHeightMod; private final HashSet skinColors; private final HashSet beardColors; private final HashSet hairColors; private final ArrayList baseClasses; private final ArrayList skillsGranted; private final ArrayList powersGranted; private mbEnums.RaceType raceType; private int token = 0; private HashSet hairStyles; private HashSet beardStyles; private ArrayList effectsList = new ArrayList<>(); /** * ResultSet Constructor */ public Race(ResultSet rs) throws SQLException { this.raceRuneID = rs.getInt("ID"); this.raceType = mbEnums.RaceType.getRaceTypebyRuneID(raceRuneID); this.name = rs.getString("name"); this.description = rs.getString("description"); this.strStart = rs.getShort("strStart"); this.strMin = rs.getShort("strMin"); this.strMax = rs.getShort("strMax"); this.dexStart = rs.getShort("dexStart"); this.dexMin = rs.getShort("dexMin"); this.dexMax = rs.getShort("dexMax"); this.conStart = rs.getShort("conStart"); this.conMin = rs.getShort("conMin"); this.conMax = rs.getShort("conMax"); this.intStart = rs.getShort("intStart"); this.intMin = rs.getShort("intMin"); this.intMax = rs.getShort("intMax"); this.spiStart = rs.getShort("spiStart"); this.spiMin = rs.getShort("spiMin"); this.spiMax = rs.getShort("spiMax"); this.token = rs.getInt("token"); this.healthBonus = rs.getShort("healthBonus"); this.manaBonus = rs.getShort("manaBonus"); this.staminaBonus = rs.getShort("staminaBonus"); this.startingPoints = rs.getByte("startingPoints"); this.raceType = RaceType.getRaceTypebyRuneID(this.raceRuneID); this.minHeight = rs.getFloat("minHeight"); this.strHeightMod = rs.getFloat("strHeightMod"); this.hairStyles = DbManager.RaceQueries.HAIR_STYLES_FOR_RACE(raceRuneID); this.beardStyles = DbManager.RaceQueries.BEARD_STYLES_FOR_RACE(raceRuneID); this.skinColors = DbManager.RaceQueries.SKIN_COLOR_FOR_RACE(raceRuneID); this.beardColors = DbManager.RaceQueries.BEARD_COLORS_FOR_RACE(raceRuneID); this.hairColors = DbManager.RaceQueries.HAIR_COLORS_FOR_RACE(raceRuneID); this.baseClasses = DbManager.BaseClassQueries.GET_BASECLASS_FOR_RACE(raceRuneID); this.skillsGranted = DbManager.SkillReqQueries.GET_REQS_FOR_RUNE(raceRuneID); this.powersGranted = PowerReq.getPowerReqsForRune(raceRuneID); this.effectsList = DbManager.MobBaseQueries.GET_RUNEBASE_EFFECTS(this.raceRuneID); } /** * Rune Template Constructor * minHeight and strHeightMod are server-only and come from the static_rune_race row. */ public Race(RuneTemplate rune, Race dbRace) { this.raceRuneID = rune.template_id; this.raceType = mbEnums.RaceType.getRaceTypebyRuneID(raceRuneID); this.name = rune.obj_name; this.description = rune.rune_dsc; this.strStart = rune.rune_attr_adj.getOrDefault(mbEnums.AttributeType.Strength, 0).shortValue(); this.strMin = 0; this.strMax = rune.rune_max_attr_adj.getOrDefault(mbEnums.AttributeType.Strength, 0).shortValue(); this.dexStart = rune.rune_attr_adj.getOrDefault(mbEnums.AttributeType.Dexterity, 0).shortValue(); this.dexMin = 0; this.dexMax = rune.rune_max_attr_adj.getOrDefault(mbEnums.AttributeType.Dexterity, 0).shortValue(); this.conStart = rune.rune_attr_adj.getOrDefault(mbEnums.AttributeType.Constitution, 0).shortValue(); this.conMin = 0; this.conMax = rune.rune_max_attr_adj.getOrDefault(mbEnums.AttributeType.Constitution, 0).shortValue(); this.intStart = rune.rune_attr_adj.getOrDefault(mbEnums.AttributeType.Intelligence, 0).shortValue(); this.intMin = 0; this.intMax = rune.rune_max_attr_adj.getOrDefault(mbEnums.AttributeType.Intelligence, 0).shortValue(); this.spiStart = rune.rune_attr_adj.getOrDefault(mbEnums.AttributeType.Spirit, 0).shortValue(); this.spiMin = 0; this.spiMax = rune.rune_max_attr_adj.getOrDefault(mbEnums.AttributeType.Spirit, 0).shortValue(); this.token = Hasher.SBStringHash(rune.rune_sub_type); this.healthBonus = (short) rune.rune_health; this.manaBonus = (short) rune.rune_mana; this.staminaBonus = (short) rune.rune_stamina; this.startingPoints = (byte) (STARTING_POINT_POOL - rune.rune_creation_cost); this.minHeight = dbRace.minHeight; this.strHeightMod = dbRace.strHeightMod; this.hairStyles = new HashSet<>(rune.rune_hair); this.beardStyles = new HashSet<>(rune.rune_beard); this.skinColors = DbManager.RaceQueries.SKIN_COLOR_FOR_RACE(raceRuneID); this.beardColors = DbManager.RaceQueries.BEARD_COLORS_FOR_RACE(raceRuneID); this.hairColors = DbManager.RaceQueries.HAIR_COLORS_FOR_RACE(raceRuneID); this.baseClasses = getBaseClassesForRace(rune); this.skillsGranted = DbManager.SkillReqQueries.GET_REQS_FOR_RUNE(raceRuneID); this.powersGranted = PowerReq.getPowerReqsForRune(raceRuneID); this.effectsList = DbManager.MobBaseQueries.GET_RUNEBASE_EFFECTS(this.raceRuneID); // Races without hair styles (Aracoix, Shade) select style 0, which creation validates if (this.hairStyles.isEmpty()) this.hairStyles.add(0); } // Base classes are the creation class runes whose item_race_req names this race's sub type. // Every creation class rune's race list is a whitelist (restrict false). private static ArrayList getBaseClassesForRace(RuneTemplate rune) { ArrayList baseClasses = new ArrayList<>(); mbEnums.MonsterType monsterType; BaseClass baseClass; monsterType = CacheParserUtil.readEnum(mbEnums.MonsterType.class, rune.rune_sub_type, rune.template_id, "rune_sub_type"); for (RuneTemplate classRune : RuneTemplate._runes.values()) { if (classRune.rune_type != mbEnums.RuneType.CLASS || !classRune.rune_is_standard_character_creation) continue; if (!classRune.item_race_req.contains(monsterType)) continue; baseClass = DbManager.BaseClassQueries.GET_BASE_CLASS(classRune.template_id); if (baseClass != null) baseClasses.add(baseClass); } return baseClasses; } // Iterates the static_rune_race rows: no rune field separates 2018-2021 from the mob race runes, // and each row still holds the server-only heights. public static void loadAllRaces() { ConcurrentHashMap dbRaces; ConcurrentHashMap races; RuneTemplate rune; dbRaces = DbManager.RaceQueries.LOAD_ALL_RACES(); races = new ConcurrentHashMap<>(MBServerStatics.CHM_INIT_CAP, MBServerStatics.CHM_LOAD, MBServerStatics.CHM_THREAD_LOW); for (Race dbRace : dbRaces.values()) { rune = RuneTemplate._runes.get(dbRace.raceRuneID); if (rune == null || rune.rune_type != mbEnums.RuneType.RACE) { Logger.warn("Race " + dbRace.raceRuneID + " has no race rune; loaded from static_rune_race"); races.put(dbRace.raceRuneID, dbRace); continue; } races.put(dbRace.raceRuneID, new Race(rune, dbRace)); } Race._raceByID = races; Logger.info("Loaded " + races.size() + " races from runes"); } public static Race getRace(int id) { return _raceByID.get(id); } @Override public boolean equals(Object object) { if ((object instanceof Race) == false) return false; Race race = (Race) object; return this.raceRuneID == race.raceRuneID; } @Override public int hashCode() { return this.raceRuneID; } /* * Getters */ public String getName() { return name; } public String getDescription() { return description; } public short getStrStart() { return strStart; } public short getStrMin() { return strMin; } public short getStrMax() { return strMax; } public short getDexStart() { return dexStart; } public short getDexMin() { return dexMin; } public short getDexMax() { return dexMax; } public short getConStart() { return conStart; } public short getConMin() { return conMin; } public short getConMax() { return conMax; } public short getIntStart() { return intStart; } public short getIntMin() { return intMin; } public short getIntMax() { return intMax; } public short getSpiStart() { return spiStart; } public short getSpiMin() { return spiMin; } public short getSpiMax() { return spiMax; } public byte getStartingPoints() { return startingPoints; } public int getToken() { return token; } public final HashSet getHairStyles() { return hairStyles; } public final HashSet getBeardStyles() { return beardStyles; } public final HashSet getBeardColors() { return beardColors; } public HashSet getHairColors() { return hairColors; } public HashSet getSkinColors() { return skinColors; } public int getNumSkinColors() { return this.skinColors.size(); } public int getNumHairColors() { return this.hairColors.size(); } public int getNumBeardColors() { return this.beardColors.size(); } public final ArrayList getValidBaseClasses() { return baseClasses; } public ArrayList getAllowedRunes() { return RuneBase.AllowedRaceRunesMap.get(raceRuneID); } public ArrayList getSkillsGranted() { return this.skillsGranted; } public ArrayList getPowersGranted() { return this.powersGranted; } public ArrayList getEffectsGranted() { return RuneBaseEffect.RuneIDBaseEffectMap.get(this.raceRuneID); } /* * Validators */ public boolean isValidBeardStyle(int id) { return this.beardStyles.contains(id); } public boolean isValidBeardColor(int id) { return this.beardColors.contains(id); } public boolean isValidHairStyle(int id) { return this.hairStyles.contains(id); } public boolean isValidHairColor(int id) { return this.hairColors.contains(id); } public boolean isValidSkinColor(int id) { return this.skinColors.contains(id); } public boolean isAllowedRune(RuneBase rb) { if (this.getAllowedRunes() != null) if (this.getAllowedRunes().contains(rb.getObjectUUID())) return true; if (RuneBase.AllowedBaseClassRunesMap.containsKey(111111)) { if (RuneBase.AllowedRaceRunesMap.get(111111).contains(rb.getObjectUUID())) return true; } return false; } public float getHealthBonus() { return this.healthBonus; } public float getManaBonus() { return this.manaBonus; } public float getStaminaBonus() { return this.staminaBonus; } public float getMinHeight() { return this.minHeight; } public float getStrHeightMod() { return this.strHeightMod; } public float getHeight(short str) { return this.minHeight + (this.strHeightMod * str); } /* * Serializing */ public float getCenterHeight(short str) { return getHeight(str) / 2f; } public void serializeForClientMsg(ByteBufferWriter writer) { writer.putInt(1); // For Race writer.putInt(0); // Pad writer.putInt(this.raceRuneID); writer.putInt(mbEnums.GameObjectType.Race.ordinal()); writer.putInt(raceRuneID); } public int getRaceRuneID() { return raceRuneID; } public mbEnums.RaceType getRaceType() { return raceType; } public ArrayList getEffectsList() { return effectsList; } }