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

+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) {