Build static NPCs from zone mobiles with their CZONE runes and equipment
NPC.createStaticNPC fills a TRAINER zone mobile; static NPCs send the CZONE rune list ordered by kind (RuneType.mobileRuneKind) and wear the mobile_equipment list. DB NPCs keep the contract rune rebuild. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2fcdcf9f0d
commit
6a3480a0dd
4 files changed
+242
-102
No files matched your search
@@ -510,6 +510,16 @@ public enum NPCManager {
|
||||
|
||||
public static void applyRunesForNPC(NPC npc) {
|
||||
npc.runes = new ArrayList<>();
|
||||
|
||||
if (!npc.staticRuneIDs.isEmpty()) {
|
||||
for (int runeID : npc.staticRuneIDs) {
|
||||
RuneBase rb = RuneBase.getRuneBase(runeID);
|
||||
if (rb != null)
|
||||
npc.runes.add(new CharacterRune(rb, npc.getObjectUUID()));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
RuneBase shopkeeperBase = RuneBase.getRuneBase(252620);
|
||||
CharacterRune shopkeeper = new CharacterRune(shopkeeperBase, npc.getObjectUUID());
|
||||
npc.runes.add(shopkeeper);
|
||||
|
||||
+11
-4
@@ -1471,10 +1471,17 @@ public class mbEnums {
|
||||
// rune_type values from the client cache data (COBJECTS/RUNE)
|
||||
|
||||
public enum RuneType {
|
||||
CLASS,
|
||||
DISCIPLINE,
|
||||
RACE,
|
||||
TALENT
|
||||
CLASS(3),
|
||||
DISCIPLINE(4),
|
||||
RACE(1),
|
||||
TALENT(5);
|
||||
|
||||
// Rune kind live servers sent for mobs and NPCs (World Boot Plan findings §5, Task 3.1); players send base classes as 2
|
||||
public final int mobileRuneKind;
|
||||
|
||||
RuneType(int mobileRuneKind) {
|
||||
this.mobileRuneKind = mobileRuneKind;
|
||||
}
|
||||
}
|
||||
|
||||
public enum ShrineType {
|
||||
|
||||
@@ -16,6 +16,7 @@ import engine.loot.BootySetEntry;
|
||||
import engine.math.Vector3fImmutable;
|
||||
import engine.mbEnums;
|
||||
import engine.parsers.cache.CacheParserUtil;
|
||||
import engine.parsers.objects.CZoneDrop;
|
||||
import engine.parsers.objects.RuneTemplate;
|
||||
import engine.server.MBServerStatics;
|
||||
import org.pmw.tinylog.Logger;
|
||||
@@ -158,6 +159,27 @@ public class MobBase extends AbstractGameObject {
|
||||
return equip;
|
||||
}
|
||||
|
||||
// Worn items straight from a zone mobile's mobile_equipment list (World Boot Plan 3.2); no booty set ID
|
||||
public static ConcurrentHashMap<mbEnums.EquipSlotType, Item> loadEquipment(ArrayList<CZoneDrop> equipmentList) {
|
||||
|
||||
ConcurrentHashMap<mbEnums.EquipSlotType, Item> equip = new ConcurrentHashMap<>();
|
||||
|
||||
for (CZoneDrop equipmentEntry : equipmentList) {
|
||||
|
||||
Item item = new Item(equipmentEntry.item_id);
|
||||
|
||||
if (item.template.getItemEqSlotsOr().isEmpty())
|
||||
continue;
|
||||
|
||||
item.objectUUID = ItemManager.lastNegativeID.decrementAndGet();
|
||||
item.drop_chance = equipmentEntry.chance;
|
||||
item.equipSlot = item.template.getItemEqSlotsOr().iterator().next();
|
||||
equip.put(item.equipSlot, item);
|
||||
}
|
||||
|
||||
return equip;
|
||||
}
|
||||
|
||||
public static MobBase getMobBase(int id) {
|
||||
return DbManager.MobBaseQueries.GET_MOBBASE(id);
|
||||
}
|
||||
|
||||
+199
-98
@@ -25,6 +25,10 @@ import engine.net.Dispatch;
|
||||
import engine.net.client.msg.PetMsg;
|
||||
import engine.net.client.msg.PlaceAssetMsg;
|
||||
import engine.parsers.objects.CObjectTemplate;
|
||||
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.joda.time.DateTime;
|
||||
import org.pmw.tinylog.Logger;
|
||||
@@ -44,6 +48,7 @@ import static engine.util.StringUtils.wordCount;
|
||||
public class NPC extends AbstractCharacter {
|
||||
|
||||
public static int SVR_CLOSE_WINDOW = 4;
|
||||
public static final int SHOPKEEPER_RUNE = 252620;
|
||||
public static HashMap<Integer, ArrayList<String>> _pirateNames = new HashMap<>();
|
||||
// Used for thread safety
|
||||
public final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
|
||||
@@ -74,6 +79,9 @@ public class NPC extends AbstractCharacter {
|
||||
public int parentZoneUUID;
|
||||
public int equipmentSetID = 0;
|
||||
private int repairCost = 5;
|
||||
// Static zone trainers (World Boot Plan 3.2): the CZONE rune list as given, and worn equipment
|
||||
public final ArrayList<Integer> staticRuneIDs = new ArrayList<>();
|
||||
private ArrayList<CZoneDrop> staticEquipment = null;
|
||||
|
||||
// New NPC constructor. Fill in the blanks and then call
|
||||
// PERSIST.
|
||||
@@ -188,106 +196,18 @@ public class NPC extends AbstractCharacter {
|
||||
|
||||
public static void serializeForClientMsgOtherPlayer(NPC npc, ByteBufferWriter writer) {
|
||||
|
||||
writer.putInt(0);
|
||||
writer.putInt(0);
|
||||
|
||||
//num Runes
|
||||
int cnt = 3;
|
||||
boolean isVamp = false, isHealer = false, isArcher = false, isTrainer = false;
|
||||
int contractID = 0, classID = 0;
|
||||
int extraRune = 0;
|
||||
|
||||
if (npc.contract != null) {
|
||||
contractID = npc.contract.getContractID();
|
||||
classID = npc.contract.getClassID();
|
||||
extraRune = npc.contract.getExtraRune();
|
||||
|
||||
if (extraRune == contractID)
|
||||
extraRune = 0;
|
||||
|
||||
}
|
||||
|
||||
if ((contractID > 252642 && contractID < 252647) || contractID == 252652) {
|
||||
isVamp = true;
|
||||
cnt++;
|
||||
}
|
||||
|
||||
if (contractID == 252582 || contractID == 252579 || contractID == 252581
|
||||
|| contractID == 252584 || contractID == 252597 || contractID == 252598
|
||||
|| contractID == 252628 || extraRune == 252582 || extraRune == 252579
|
||||
|| extraRune == 252581 || extraRune == 252584 || extraRune == 252597
|
||||
|| extraRune == 252598 || extraRune == 252628) {
|
||||
isHealer = true;
|
||||
cnt++;
|
||||
}
|
||||
|
||||
if (contractID == 252570) {
|
||||
isArcher = true;
|
||||
cnt++;
|
||||
}
|
||||
|
||||
if (classID != 0)
|
||||
cnt++;
|
||||
|
||||
if (extraRune != 0 && extraRune != contractID)
|
||||
cnt++;
|
||||
|
||||
writer.putInt(cnt);
|
||||
|
||||
//Race
|
||||
writer.putInt(1);
|
||||
writer.putInt(0);
|
||||
|
||||
if (npc.mobBase != null)
|
||||
writer.putInt(npc.mobBase.getObjectUUID());
|
||||
else
|
||||
writer.putInt(2011);
|
||||
|
||||
writer.putInt(GameObjectType.NPCRaceRune.ordinal());
|
||||
writer.putInt(npc.currentID);
|
||||
|
||||
//Class/Trainer/Whatever
|
||||
writer.putInt(5);
|
||||
writer.putInt(0);
|
||||
int classID = 0;
|
||||
|
||||
if (npc.contract != null)
|
||||
writer.putInt(contractID);
|
||||
else
|
||||
writer.putInt(2500);
|
||||
classID = npc.contract.getClassID();
|
||||
|
||||
writer.putInt(GameObjectType.NPCClassRune.ordinal());
|
||||
writer.putInt(npc.currentID);
|
||||
|
||||
//vampire trainer
|
||||
cnt = 0;
|
||||
|
||||
if (extraRune != 0)
|
||||
cnt = serializeExtraRune(npc, extraRune, cnt, writer);
|
||||
if (isVamp)
|
||||
cnt = serializeExtraRune(npc, 252647, cnt, writer);
|
||||
|
||||
//Healer trainer
|
||||
if (isHealer)
|
||||
cnt = serializeExtraRune(npc, 252592, cnt, writer);
|
||||
|
||||
if (classID != 0) {
|
||||
writer.putInt(4);
|
||||
writer.putInt(0);
|
||||
writer.putInt(classID);
|
||||
writer.putInt(GameObjectType.NPCExtraRune.ordinal());
|
||||
writer.putInt(npc.currentID);
|
||||
}
|
||||
|
||||
//Scout trainer
|
||||
if (isArcher)
|
||||
cnt = serializeExtraRune(npc, 252654, cnt, writer);
|
||||
|
||||
//Shopkeeper
|
||||
writer.putInt(5);
|
||||
writer.putInt(0);
|
||||
writer.putInt(0x3DACC);
|
||||
writer.putInt(GameObjectType.NPCShopkeeperRune.ordinal());
|
||||
writer.putInt(npc.currentID);
|
||||
writer.putInt(0);
|
||||
|
||||
if (npc.staticRuneIDs.isEmpty())
|
||||
serializeContractRunes(npc, writer);
|
||||
else
|
||||
serializeStaticRunes(npc, writer);
|
||||
|
||||
//Send Stats
|
||||
writer.putInt(5);
|
||||
@@ -431,6 +351,140 @@ public class NPC extends AbstractCharacter {
|
||||
writer.put((byte) 0);
|
||||
}
|
||||
|
||||
// DB NPCs rebuild their runes from the contract
|
||||
private static void serializeContractRunes(NPC npc, ByteBufferWriter writer) {
|
||||
|
||||
int cnt = 3;
|
||||
boolean isVamp = false, isHealer = false, isArcher = false, isTrainer = false;
|
||||
int contractID = 0, classID = 0;
|
||||
int extraRune = 0;
|
||||
|
||||
if (npc.contract != null) {
|
||||
contractID = npc.contract.getContractID();
|
||||
classID = npc.contract.getClassID();
|
||||
extraRune = npc.contract.getExtraRune();
|
||||
|
||||
if (extraRune == contractID)
|
||||
extraRune = 0;
|
||||
|
||||
}
|
||||
|
||||
if ((contractID > 252642 && contractID < 252647) || contractID == 252652) {
|
||||
isVamp = true;
|
||||
cnt++;
|
||||
}
|
||||
|
||||
if (contractID == 252582 || contractID == 252579 || contractID == 252581
|
||||
|| contractID == 252584 || contractID == 252597 || contractID == 252598
|
||||
|| contractID == 252628 || extraRune == 252582 || extraRune == 252579
|
||||
|| extraRune == 252581 || extraRune == 252584 || extraRune == 252597
|
||||
|| extraRune == 252598 || extraRune == 252628) {
|
||||
isHealer = true;
|
||||
cnt++;
|
||||
}
|
||||
|
||||
if (contractID == 252570) {
|
||||
isArcher = true;
|
||||
cnt++;
|
||||
}
|
||||
|
||||
if (classID != 0)
|
||||
cnt++;
|
||||
|
||||
if (extraRune != 0 && extraRune != contractID)
|
||||
cnt++;
|
||||
|
||||
writer.putInt(cnt);
|
||||
|
||||
//Race
|
||||
writer.putInt(1);
|
||||
writer.putInt(0);
|
||||
|
||||
if (npc.mobBase != null)
|
||||
writer.putInt(npc.mobBase.getObjectUUID());
|
||||
else
|
||||
writer.putInt(2011);
|
||||
|
||||
writer.putInt(GameObjectType.NPCRaceRune.ordinal());
|
||||
writer.putInt(npc.currentID);
|
||||
|
||||
//Class/Trainer/Whatever
|
||||
writer.putInt(5);
|
||||
writer.putInt(0);
|
||||
|
||||
if (npc.contract != null)
|
||||
writer.putInt(contractID);
|
||||
else
|
||||
writer.putInt(2500);
|
||||
|
||||
writer.putInt(GameObjectType.NPCClassRune.ordinal());
|
||||
writer.putInt(npc.currentID);
|
||||
|
||||
//vampire trainer
|
||||
cnt = 0;
|
||||
|
||||
if (extraRune != 0)
|
||||
cnt = serializeExtraRune(npc, extraRune, cnt, writer);
|
||||
if (isVamp)
|
||||
cnt = serializeExtraRune(npc, 252647, cnt, writer);
|
||||
|
||||
//Healer trainer
|
||||
if (isHealer)
|
||||
cnt = serializeExtraRune(npc, 252592, cnt, writer);
|
||||
|
||||
if (classID != 0) {
|
||||
writer.putInt(4);
|
||||
writer.putInt(0);
|
||||
writer.putInt(classID);
|
||||
writer.putInt(GameObjectType.NPCExtraRune.ordinal());
|
||||
writer.putInt(npc.currentID);
|
||||
}
|
||||
|
||||
//Scout trainer
|
||||
if (isArcher)
|
||||
cnt = serializeExtraRune(npc, 252654, cnt, writer);
|
||||
|
||||
//Shopkeeper
|
||||
writer.putInt(5);
|
||||
writer.putInt(0);
|
||||
writer.putInt(0x3DACC);
|
||||
writer.putInt(GameObjectType.NPCShopkeeperRune.ordinal());
|
||||
writer.putInt(npc.currentID);
|
||||
|
||||
}
|
||||
|
||||
// Static trainers send the CZONE rune list ordered by kind, as live (World Boot Plan findings §5, Task 3.1)
|
||||
private static void serializeStaticRunes(NPC npc, ByteBufferWriter writer) {
|
||||
|
||||
ArrayList<Integer> runeIDs = npc.getStaticRunesByKind();
|
||||
GameObjectType[] otherRuneTypes = {GameObjectType.NPCClassRune, GameObjectType.NPCClassRuneTwo,
|
||||
GameObjectType.NPCClassRuneThree, GameObjectType.NPCExtraRune};
|
||||
GameObjectType runeObjectType;
|
||||
RuneTemplate rune;
|
||||
int otherRunes = 0;
|
||||
|
||||
writer.putInt(runeIDs.size());
|
||||
|
||||
for (int runeID : runeIDs) {
|
||||
|
||||
rune = RuneTemplate._runes.get(runeID);
|
||||
|
||||
// Each rune object needs its own type, since all share the NPC's ID
|
||||
if (rune.rune_type == RuneType.RACE)
|
||||
runeObjectType = GameObjectType.NPCRaceRune;
|
||||
else if (runeID == SHOPKEEPER_RUNE)
|
||||
runeObjectType = GameObjectType.NPCShopkeeperRune;
|
||||
else
|
||||
runeObjectType = otherRuneTypes[Math.min(otherRunes++, otherRuneTypes.length - 1)];
|
||||
|
||||
writer.putInt(rune.rune_type.mobileRuneKind);
|
||||
writer.putInt(0);
|
||||
writer.putInt(runeID);
|
||||
writer.putInt(runeObjectType.ordinal());
|
||||
writer.putInt(npc.currentID);
|
||||
}
|
||||
}
|
||||
|
||||
private static int serializeExtraRune(NPC npc, int runeID, int cnt, ByteBufferWriter writer) {
|
||||
|
||||
writer.putInt(5);
|
||||
@@ -488,6 +542,39 @@ public class NPC extends AbstractCharacter {
|
||||
return npc;
|
||||
}
|
||||
|
||||
// World Boot Plan 3.2: a TRAINER zone mobile. The caller adds it to the cache and runs runAfterLoad.
|
||||
public static NPC createStaticNPC(int npcID, Zone parentZone, CZoneMobile mobile, int contractID, Building building, Guild guild) {
|
||||
|
||||
NPC npc = new NPC();
|
||||
CZonePlacement placement = mobile.mobile_base_zone;
|
||||
|
||||
npc.setObjectUUID(npcID);
|
||||
npc.currentID = npcID;
|
||||
npc.setStaticWorldObject(true);
|
||||
npc.setObjectTypeMask(MBServerStatics.MASK_NPC);
|
||||
npc.gridObjectType = GridObjectType.STATIC;
|
||||
npc.parentZoneUUID = parentZone.getObjectUUID();
|
||||
npc.name = placement.basic_zone_name_override;
|
||||
npc.contractUUID = contractID;
|
||||
npc.loadID = mobile.mobile_rune_stone_ids.get(0);
|
||||
npc.level = (short) mobile.mobile_level;
|
||||
npc.buildingUUID = building == null ? 0 : building.getObjectUUID();
|
||||
npc.guildUUID = guild == null ? 0 : guild.getObjectUUID();
|
||||
npc.statLat = placement.basic_zone_spawn_location.x;
|
||||
npc.statAlt = placement.basic_zone_spawn_location.y;
|
||||
npc.statLon = placement.basic_zone_spawn_location.z;
|
||||
npc.buyPercent = .33f;
|
||||
npc.sellPercent = 1;
|
||||
|
||||
// W8: zone mobiles face π − y_rot, like props
|
||||
npc.setRotation(Quaternion.fromYaw((float) (Math.PI - placement.basic_zone_y_rot)));
|
||||
|
||||
npc.staticRuneIDs.addAll(mobile.mobile_rune_stone_ids);
|
||||
npc.staticEquipment = mobile.mobile_equipment;
|
||||
|
||||
return npc;
|
||||
}
|
||||
|
||||
public static NPC getNPC(int id) {
|
||||
|
||||
if (id == 0)
|
||||
@@ -563,6 +650,16 @@ public class NPC extends AbstractCharacter {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Stable sort: CZONE order within a kind
|
||||
public ArrayList<Integer> getStaticRunesByKind() {
|
||||
|
||||
ArrayList<Integer> runeIDs = new ArrayList<>(this.staticRuneIDs);
|
||||
|
||||
runeIDs.sort((first, second) -> Integer.compare(RuneTemplate._runes.get(first).rune_type.mobileRuneKind,
|
||||
RuneTemplate._runes.get(second).rune_type.mobileRuneKind));
|
||||
return runeIDs;
|
||||
}
|
||||
|
||||
public boolean isStatic() {
|
||||
return this.isStatic;
|
||||
}
|
||||
@@ -787,7 +884,8 @@ public class NPC extends AbstractCharacter {
|
||||
|
||||
this.contract = DbManager.ContractQueries.GET_CONTRACT(this.contractUUID);
|
||||
|
||||
if (this.equipmentSetID == 0 && this.contract != null)
|
||||
// Static trainers wear only their CZONE equipment
|
||||
if (this.equipmentSetID == 0 && this.contract != null && this.staticEquipment == null)
|
||||
this.equipmentSetID = this.contract.equipmentSet;
|
||||
|
||||
// Default to contract load ID
|
||||
@@ -883,7 +981,10 @@ public class NPC extends AbstractCharacter {
|
||||
if (equipmentSetID != 0 && LootManager._bootySetMap.get(equipmentSetID) == null)
|
||||
Logger.error("Invalid equipSet: " + equipmentSetID + " contract: " + this.contractUUID + " npc: " + this.getObjectUUID());
|
||||
|
||||
this.charItemManager.equipped = loadEquipmentSet(this.equipmentSetID);
|
||||
if (this.staticEquipment != null)
|
||||
this.charItemManager.equipped = MobBase.loadEquipment(this.staticEquipment);
|
||||
else
|
||||
this.charItemManager.equipped = loadEquipmentSet(this.equipmentSetID);
|
||||
|
||||
// Create NPC bounds object
|
||||
Bounds npcBounds = Bounds.borrow();
|
||||
|
||||
Reference in new issue
Block a user