Derive mob attributes from every rune the client is sent

Client ArcCharacter_RecalcMaxAttributes (0x470440) and RecalcAttributes
(0x470610) treat the stats in the mob message as spent points and add
rune_attr_adj from all of the character's runes, clamped to
[1, max(1, sum of rune_max_attr_adj)]. The server summed only the race
rune and sent the result, so the client counted it twice.

Mob.getClientRuneIDs lists the runes serializeForClientMsgOtherPlayer
sends (race, plus Pet 2522 for pets, Warrior 2518 + Guard 252621 for
contract and guard mobs); getRuneAttribute applies the client rule.
calculateModifiedStats and the mob branch of CharacterSkill use it,
and the message now sends 0 (no spent points), as NPCs already do.
MobBaseStats (race rune only) has no users left and is removed.

Value changes (server side):
- Contract mobs and player guards: +100 to all five attributes
  (Guard rune 252621; its max adj is also +100, so no new clamp).
- Pets: + Pet rune 2522 (Str 76, Dex 76, Con 120, Int 40, Spi 40;
  max Str 500, Dex 500, Con 2000, Int 40, Spi 40).
- The new max clamp changes no loaded mobbase on its own (checked all
  2375 against their race runes).
These feed mob skill values (CharacterSkill) and statXxxCurrent.
Guard ATR/defense/damage/health still come from the NPCManager guard
formulas.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
MagicBotandClaude Opus 5.5 committed 2026-10-24 11:42:48 -04:00
1 parent 5c58586f89
commit b0460ed2d0
4 files changed
+55 -93

No files matched your search

+5 -5
View File
@@ -1043,15 +1043,15 @@ public class CharacterSkill extends AbstractGameObject {
if (this.skillsBase.getStrMod() > 0)
statMod += (float) this.skillsBase.getStrMod() * (float) ((Mob) CharacterSkill.GetOwner(this)).getMobBase().getMobBaseStats().getBaseStr() / 100f;
statMod += (float) this.skillsBase.getStrMod() * (float) ((Mob) CharacterSkill.GetOwner(this)).getRuneAttribute(mbEnums.AttributeType.Strength) / 100f;
if (this.skillsBase.getDexMod() > 0)
statMod += (float) this.skillsBase.getDexMod() * (float) ((Mob) CharacterSkill.GetOwner(this)).getMobBase().getMobBaseStats().getBaseDex() / 100f;
statMod += (float) this.skillsBase.getDexMod() * (float) ((Mob) CharacterSkill.GetOwner(this)).getRuneAttribute(mbEnums.AttributeType.Dexterity) / 100f;
if (this.skillsBase.getConMod() > 0)
statMod += (float) this.skillsBase.getConMod() * (float) ((Mob) CharacterSkill.GetOwner(this)).getMobBase().getMobBaseStats().getBaseCon() / 100f;
statMod += (float) this.skillsBase.getConMod() * (float) ((Mob) CharacterSkill.GetOwner(this)).getRuneAttribute(mbEnums.AttributeType.Constitution) / 100f;
if (this.skillsBase.getIntMod() > 0)
statMod += (float) this.skillsBase.getIntMod() * (float) ((Mob) CharacterSkill.GetOwner(this)).getMobBase().getMobBaseStats().getBaseInt() / 100f;
statMod += (float) this.skillsBase.getIntMod() * (float) ((Mob) CharacterSkill.GetOwner(this)).getRuneAttribute(mbEnums.AttributeType.Intelligence) / 100f;
if (this.skillsBase.getSpiMod() > 0)
statMod += (float) this.skillsBase.getSpiMod() * (float) ((Mob) CharacterSkill.GetOwner(this)).getMobBase().getMobBaseStats().getBaseSpi() / 100f;
statMod += (float) this.skillsBase.getSpiMod() * (float) ((Mob) CharacterSkill.GetOwner(this)).getRuneAttribute(mbEnums.AttributeType.Spirit) / 100f;
if (statMod < 1)
statMod = 1f;
else if (statMod > 600)
+50 -11
View File
@@ -22,6 +22,7 @@ import engine.net.ByteBufferWriter;
import engine.net.Dispatch;
import engine.net.client.msg.PetMsg;
import engine.net.client.msg.PlaceAssetMsg;
import engine.parsers.objects.RuneTemplate;
import engine.server.MBServerStatics;
import org.jetbrains.annotations.NotNull;
import org.joda.time.DateTime;
@@ -29,6 +30,7 @@ import org.pmw.tinylog.Logger;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Delayed;
@@ -198,19 +200,19 @@ public class Mob extends AbstractIntelligenceAgent implements Delayed {
writer.putInt(mob.getObjectType().ordinal());
writer.putInt(mob.currentID);
//Send Stats
// Stats are the base (spent points, 0 for mobs); the client adds every sent rune's attr adj (0x470610)
writer.putInt(5);
writer.putInt(MBServerStatics.STAT_STR_ID);
writer.putInt(mob.statStrCurrent);
writer.putInt(0);
writer.putInt(MBServerStatics.STAT_DEX_ID);
writer.putInt(mob.statDexCurrent);
writer.putInt(0);
writer.putInt(MBServerStatics.STAT_CON_ID);
writer.putInt(mob.statConCurrent);
writer.putInt(0);
writer.putInt(MBServerStatics.STAT_INT_ID);
writer.putInt(mob.statIntCurrent);
writer.putInt(0);
writer.putInt(MBServerStatics.STAT_SPI_ID);
writer.putInt(mob.statSpiCurrent);
writer.putInt(0);
writer.putString(mob.firstName);
@@ -725,13 +727,50 @@ public class Mob extends AbstractIntelligenceAgent implements Delayed {
return this.bindLoc;
}
// The runes serializeForClientMsgOtherPlayer sends; the client derives the mob's attributes from all of them
public ArrayList<Integer> getClientRuneIDs() {
ArrayList<Integer> runeIDs = new ArrayList<>();
if (this.isPet())
runeIDs.add(2522);
else if (this.contract != null || this.isPlayerGuard()) {
runeIDs.add(2518);
runeIDs.add(252621);
}
runeIDs.add((this.mobBase != null) ? this.mobBase.getObjectUUID() : this.loadID);
return runeIDs;
}
// Client ArcCharacter_RecalcMaxAttributes 0x470440 + RecalcAttributes 0x470610 with no spent points
public int getRuneAttribute(AttributeType attributeType) {
RuneTemplate rune;
int value = 0;
int max = 0;
for (int runeID : this.getClientRuneIDs()) {
rune = RuneTemplate._runes.get(runeID);
if (rune == null)
continue;
value += rune.rune_attr_adj.getOrDefault(attributeType, 0);
max += rune.rune_max_attr_adj.getOrDefault(attributeType, 0);
}
max = Math.max(max, 1);
return Math.min(Math.max(value, 1), max);
}
public void calculateModifiedStats() {
float strVal = this.mobBase.getMobBaseStats().getBaseStr();
float dexVal = this.mobBase.getMobBaseStats().getBaseDex();
float conVal = this.mobBase.getMobBaseStats().getBaseCon();
float intVal = this.mobBase.getMobBaseStats().getBaseInt();
float spiVal = this.mobBase.getMobBaseStats().getBaseSpi();
float strVal = this.getRuneAttribute(AttributeType.Strength);
float dexVal = this.getRuneAttribute(AttributeType.Dexterity);
float conVal = this.getRuneAttribute(AttributeType.Constitution);
float intVal = this.getRuneAttribute(AttributeType.Intelligence);
float spiVal = this.getRuneAttribute(AttributeType.Spirit);
// TODO modify for equipment
if (this.bonuses != null) {
-6
View File
@@ -50,7 +50,6 @@ public class MobBase extends AbstractGameObject {
private int spawnTime = 0;
private float attackRange;
private boolean isNecroPet = false;
private MobBaseStats mobBaseStats;
private float walk = 0;
private float run = 0;
private float walkCombat = 0;
@@ -125,7 +124,6 @@ public class MobBase extends AbstractGameObject {
if (this.flags.contains(mbEnums.MobFlagType.RAT))
this.mask += MBServerStatics.MASK_RAT;
this.mobBaseStats = (rune != null) ? new MobBaseStats(rune) : MobBaseStats.GetGenericStats();
this.effectsList = MobBaseEffects.getEffectsForRune(this.getObjectUUID());
}
@@ -229,10 +227,6 @@ public class MobBase extends AbstractGameObject {
}
}
public MobBaseStats getMobBaseStats() {
return mobBaseStats;
}
public float getAttackRange() {
return attackRange;
}
-71
View File
@@ -1,71 +0,0 @@
// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ .
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌·
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌
// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀
// Magicbane Emulator Project © 2013 - 2026
// https://www.magicbane.com
package engine.objects;
import engine.mbEnums;
import engine.parsers.objects.RuneTemplate;
public class MobBaseStats {
public static MobBaseStats mbs = null;
private final int baseStr;
private final int baseInt;
private final int baseCon;
private final int baseSpi;
private final int baseDex;
public MobBaseStats(RuneTemplate rune) {
this.baseStr = rune.rune_attr_adj.getOrDefault(mbEnums.AttributeType.Strength, 0);
this.baseInt = rune.rune_attr_adj.getOrDefault(mbEnums.AttributeType.Intelligence, 0);
this.baseCon = rune.rune_attr_adj.getOrDefault(mbEnums.AttributeType.Constitution, 0);
this.baseSpi = rune.rune_attr_adj.getOrDefault(mbEnums.AttributeType.Spirit, 0);
this.baseDex = rune.rune_attr_adj.getOrDefault(mbEnums.AttributeType.Dexterity, 0);
}
/**
* Generic Constructor
*/
public MobBaseStats() {
this.baseStr = 0;
this.baseInt = 0;
this.baseCon = 0;
this.baseSpi = 0;
this.baseDex = 0;
}
public static MobBaseStats GetGenericStats() {
if (mbs != null)
return mbs;
mbs = new MobBaseStats();
return mbs;
}
public int getBaseStr() {
return baseStr;
}
public int getBaseInt() {
return baseInt;
}
public int getBaseCon() {
return baseCon;
}
public int getBaseSpi() {
return baseSpi;
}
public int getBaseDex() {
return baseDex;
}
}