Build static mobs from zone mobiles with their CZONE runes, equipment and booty

Mob.createStaticMob fills one member of a MOBILE zone mobile's group:
race rune as mobbase, name, mobile_level (kept over the mobbase level
when set), respawn, radius, root FSM, enemy types and π − y_rot facing.
Static mobs send the CZONE rune list ordered by kind, wear
mobile_equipment and roll mobile_booties as ITEM booty. Other mobs keep
the server-picked runes and equipment sets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
MagicBotandClaude Opus 5.5 committed 2026-10-28 10:39:44 -04:00
1 parent 820177529d
commit 0fdc96d9f2
4 files changed
+125 -39

No files matched your search

@@ -42,6 +42,14 @@ public class SimulateBootyCmd extends AbstractDevCmd {
}
}
for (BootySetEntry entry : mob.staticBooty) {
CObjectTemplate template = CObjectTemplate.lookup(entry.templateID);
if (template != null)
output += "[" + entry.bootyType + "] " + template.getItemBaseName() + " [Chance] " + entry.dropChance + newline;
}
ArrayList<Item> GlassItems = new ArrayList<Item>();
ArrayList<Item> Resources = new ArrayList<Item>();
ArrayList<Item> Runes = new ArrayList<Item>();
+3
View File
@@ -89,6 +89,9 @@ public enum LootManager {
if (mob.bootySet != 0 && _bootySetMap.containsKey(mob.bootySet) == true)
RunBootySet(_bootySetMap.get(mob.bootySet), mob, inHotzone);
if (!mob.staticBooty.isEmpty())
RunBootySet(mob.staticBooty, mob, inHotzone);
//lastly, check mobs inventory for godly or disc runes to send a server announcement
for (Item item : mob.getInventory()) {
+9
View File
@@ -9,6 +9,8 @@
package engine.loot;
import engine.parsers.objects.CZoneDrop;
import java.sql.ResultSet;
import java.sql.SQLException;
@@ -34,4 +36,11 @@ public class BootySetEntry {
this.dropChance = (rs.getFloat("dropChance"));
}
// An ITEM entry from a zone mobile's mobile_booties (World Boot Plan 3.3); the chance is a percent, as in the DB
public BootySetEntry(CZoneDrop booty) {
this.bootyType = "ITEM";
this.templateID = booty.item_id;
this.dropChance = booty.chance;
}
}
+105 -39
View File
@@ -14,6 +14,7 @@ import engine.gameManager.*;
import engine.job.JobScheduler;
import engine.jobs.DeferredPowerJob;
import engine.jobs.UpgradeNPCJob;
import engine.loot.BootySetEntry;
import engine.math.Bounds;
import engine.math.FastMath;
import engine.math.Quaternion;
@@ -24,6 +25,9 @@ import engine.net.ByteBufferWriter;
import engine.net.Dispatch;
import engine.net.client.msg.PetMsg;
import engine.net.client.msg.PlaceAssetMsg;
import engine.parsers.objects.CZoneDrop;
import engine.parsers.objects.CZoneMobile;
import engine.parsers.objects.CZonePlacement;
import engine.parsers.objects.RuneTemplate;
import engine.server.MBServerStatics;
import org.jetbrains.annotations.NotNull;
@@ -76,6 +80,9 @@ public class Mob extends AbstractIntelligenceAgent implements Delayed {
private DeferredPowerJob weaponPower;
private DateTime upgradeDateTime = null;
private boolean lootSync = false;
// Static zone mobiles (World Boot Plan 3.3): worn equipment and item booty from the CZONE entry
private ArrayList<CZoneDrop> staticEquipment = null;
public final ArrayList<BootySetEntry> staticBooty = new ArrayList<>();
// New Mobile constructor. Fill in the blanks and then call
// PERSIST.
@@ -171,39 +178,10 @@ public class Mob extends AbstractIntelligenceAgent implements Delayed {
writer.putInt(0);
writer.putInt(0);
int tid = (mob.mobBase != null) ? mob.mobBase.getObjectUUID() : 0;
if (mob.isPet()) {
writer.putInt(2);
writer.putInt(3);
writer.putInt(0);
writer.putInt(2522);
writer.putInt(GameObjectType.NPCClassRune.ordinal());
writer.putInt(mob.currentID);
} else if (tid == 100570) { //kur'adar
writer.putInt(3);
Mob.serializeRune(mob, writer, 3, GameObjectType.NPCClassRuneTwo.ordinal(), 2518); //warrior class
serializeRune(mob, writer, 5, GameObjectType.NPCClassRuneThree.ordinal(), 252621); //guard rune
} else if (tid == 100962 || tid == 100965) { //Spydraxxx the Mighty, Denigo Tantric
writer.putInt(2);
serializeRune(mob, writer, 5, GameObjectType.NPCClassRuneTwo.ordinal(), 252621); //guard rune
} else if (mob.contract != null || mob.isPlayerGuard()) {
writer.putInt(3);
serializeRune(mob, writer, 3, GameObjectType.NPCClassRuneTwo.ordinal(), 2518); //warrior class
serializeRune(mob, writer, 5, GameObjectType.NPCClassRuneThree.ordinal(), mob.getGuardRuneID());
} else
writer.putInt(1);
//Generate Race Rune
writer.putInt(1);
writer.putInt(0);
if (mob.mobBase != null)
writer.putInt(mob.mobBase.getObjectUUID());
if (mob.staticRuneIDs.isEmpty())
serializeServerRunes(mob, writer);
else
writer.putInt(mob.loadID);
writer.putInt(mob.getObjectType().ordinal());
writer.putInt(mob.currentID);
serializeStaticRunes(mob, mob.getObjectType(), mob.currentID, writer);
// Stats are the base (spent points, 0 for mobs); the client adds every sent rune's attr adj (0x470610)
@@ -390,6 +368,45 @@ public class Mob extends AbstractIntelligenceAgent implements Delayed {
writer.put((byte) 0);
}
// Mobs with no zone entry (pets, guards, minions, siege) send runes the server picks
private static void serializeServerRunes(Mob mob, ByteBufferWriter writer) {
int tid = (mob.mobBase != null) ? mob.mobBase.getObjectUUID() : 0;
if (mob.isPet()) {
writer.putInt(2);
writer.putInt(3);
writer.putInt(0);
writer.putInt(2522);
writer.putInt(GameObjectType.NPCClassRune.ordinal());
writer.putInt(mob.currentID);
} else if (tid == 100570) { //kur'adar
writer.putInt(3);
Mob.serializeRune(mob, writer, 3, GameObjectType.NPCClassRuneTwo.ordinal(), 2518); //warrior class
serializeRune(mob, writer, 5, GameObjectType.NPCClassRuneThree.ordinal(), 252621); //guard rune
} else if (tid == 100962 || tid == 100965) { //Spydraxxx the Mighty, Denigo Tantric
writer.putInt(2);
serializeRune(mob, writer, 5, GameObjectType.NPCClassRuneTwo.ordinal(), 252621); //guard rune
} else if (mob.contract != null || mob.isPlayerGuard()) {
writer.putInt(3);
serializeRune(mob, writer, 3, GameObjectType.NPCClassRuneTwo.ordinal(), 2518); //warrior class
serializeRune(mob, writer, 5, GameObjectType.NPCClassRuneThree.ordinal(), mob.getGuardRuneID());
} else
writer.putInt(1);
//Generate Race Rune
writer.putInt(1);
writer.putInt(0);
if (mob.mobBase != null)
writer.putInt(mob.mobBase.getObjectUUID());
else
writer.putInt(mob.loadID);
writer.putInt(mob.getObjectType().ordinal());
writer.putInt(mob.currentID);
}
private static void serializeRune(Mob mob, ByteBufferWriter writer, int type, int objectType, int runeID) {
writer.putInt(type);
writer.putInt(0);
@@ -436,6 +453,39 @@ public class Mob extends AbstractIntelligenceAgent implements Delayed {
return mobile;
}
// World Boot Plan 3.3: one member of a MOBILE zone mobile's group. The caller adds it to the cache and runs runAfterLoad.
public static Mob createStaticMob(int mobID, Zone parentZone, CZoneMobile zoneMobile, Building building) {
Mob mobile = new Mob();
CZonePlacement placement = zoneMobile.mobile_base_zone;
mobile.setObjectUUID(mobID);
mobile.currentID = mobID;
mobile.setStaticWorldObject(true);
mobile.loadID = zoneMobile.mobile_rune_stone_ids.get(0);
mobile.level = (short) zoneMobile.mobile_level;
mobile.parentZoneUUID = parentZone.getObjectUUID();
mobile.buildingUUID = building == null ? 0 : building.getObjectUUID();
mobile.bindLoc = placement.basic_zone_spawn_location;
mobile.spawnRadius = placement.basic_zone_spawn_radius;
mobile.spawnDelay = (int) placement.basic_zone_time_to_respawn;
mobile.firstName = placement.basic_zone_name_override;
mobile.behaviourType = zoneMobile.mobile_root_fs_mid == null ? MobBehaviourType.None : zoneMobile.mobile_root_fs_mid;
mobile.enemy.addAll(zoneMobile.mobile_enemy_monster_types);
mobile.notEnemy.addAll(zoneMobile.mobile_not_enemy_monster_types);
// W8: zone mobiles face π − y_rot, like props
mobile.setRotation(Quaternion.fromYaw((float) (Math.PI - placement.basic_zone_y_rot)));
mobile.staticRuneIDs.addAll(zoneMobile.mobile_rune_stone_ids);
mobile.staticEquipment = zoneMobile.mobile_equipment;
for (CZoneDrop booty : zoneMobile.mobile_booties)
mobile.staticBooty.add(new BootySetEntry(booty));
return mobile;
}
// MB_MINION_SPAWN_TIME (seconds) shortens guard/siege minion spawns for testing; 0 or blank keeps the normal timers.
private static int minionSpawnDelay(int defaultDelay) {
@@ -730,6 +780,9 @@ public class Mob extends AbstractIntelligenceAgent implements Delayed {
ArrayList<Integer> runeIDs = new ArrayList<>();
if (!this.staticRuneIDs.isEmpty())
return this.getStaticRunesByKind();
if (this.isPet())
runeIDs.add(2522);
else if (this.contract != null || this.isPlayerGuard()) {
@@ -1089,7 +1142,7 @@ public class Mob extends AbstractIntelligenceAgent implements Delayed {
// Reload equipment set
this.charItemManager.equipped = MobBase.loadEquipmentSet(this.equipmentSetID);
this.charItemManager.equipped = this.loadEquipment();
// Only generate loot for mobiles
@@ -1099,6 +1152,15 @@ public class Mob extends AbstractIntelligenceAgent implements Delayed {
LootManager.GenerateMobLoot(this);
}
// Static zone mobiles wear only their CZONE equipment
private ConcurrentHashMap<EquipSlotType, Item> loadEquipment() {
if (this.staticEquipment != null)
return MobBase.loadEquipment(this.staticEquipment);
return MobBase.loadEquipmentSet(this.equipmentSetID);
}
@Override
public void updateDatabase() {
}
@@ -1188,6 +1250,8 @@ public class Mob extends AbstractIntelligenceAgent implements Delayed {
@Override
public void runAfterLoad() {
boolean keepsLevel;
this.setObjectTypeMask(MBServerStatics.MASK_MOB | this.getTypeMasks());
if (ConfigManager.serverType.equals(ServerType.LOGINSERVER))
@@ -1258,11 +1322,13 @@ public class Mob extends AbstractIntelligenceAgent implements Delayed {
this.mana.set(this.manaMax);
this.stamina.set(this.staminaMax);
// Don't override level for guard minions or pets
// Don't override level for guard minions or pets, or a zone mobile's mobile_level (World Boot Plan findings §6, 3.1)
if (this.contract == null)
if (!this.agentType.equals(AIAgentType.GUARDMINION) && !this.agentType.equals(AIAgentType.PET))
this.level = (short) this.mobBase.getLevel();
keepsLevel = this.agentType.equals(AIAgentType.GUARDMINION) || this.agentType.equals(AIAgentType.PET)
|| (this.isStaticWorldObject() && this.level > 0);
if (this.contract == null && !keepsLevel)
this.level = (short) this.mobBase.getLevel();
//set bonuses
@@ -1316,8 +1382,8 @@ public class Mob extends AbstractIntelligenceAgent implements Delayed {
this.charItemManager.load();
this.loadInventory();
if (this.equipmentSetID != 0)
this.charItemManager.equipped = MobBase.loadEquipmentSet(this.equipmentSetID);
if (this.equipmentSetID != 0 || this.staticEquipment != null)
this.charItemManager.equipped = this.loadEquipment();
// Combine mobbase and mob aggro arrays into one bitvector
//skip for pets