Files
Server/src/engine/gameManager/BuildingManager.java
T
MagicBotandClaude Opus 5.5 875809efe0 Look up resource templates at loot time instead of caching them on ResourceType
ResourceType.template was filled in the enum constructor, which can run
before ItemTemplate.templates is loaded, leaving it null. lootBuilding was
its only reader and threw on the first non-gold resource.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 09:38:55 -04:00

871 lines
28 KiB
Java

// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ .
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌·
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌
// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀
// Magicbane Emulator Project © 2013 - 2026
// https://www.magicbane.com
package engine.gameManager;
import engine.InterestManagement.InterestManager;
import engine.InterestManagement.WorldGrid;
import engine.job.JobContainer;
import engine.job.JobScheduler;
import engine.jobs.UpgradeBuildingJob;
import engine.loot.WorkOrder;
import engine.math.Bounds;
import engine.math.Vector3fImmutable;
import engine.mbEnums;
import engine.mbEnums.BuildingGroup;
import engine.mbEnums.GameObjectType;
import engine.net.client.msg.ErrorPopupMsg;
import engine.objects.*;
import engine.server.MBServerStatics;
import org.pmw.tinylog.Logger;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadLocalRandom;
public enum BuildingManager {
BUILDINGMANAGER;
public static HashMap<Integer, ArrayList<BuildingLocation>> _stuckLocations = new HashMap<>();
public static HashMap<Integer, ArrayList<BuildingLocation>> _slotLocations = new HashMap<>();
public static HashMap<Integer, ConcurrentHashMap<Integer, BuildingFriends>> _buildingFriends = new HashMap<>();
public static HashMap<Integer, ConcurrentHashMap<Integer, Condemned>> _buildingCondemned = new HashMap<>();
public static HashMap<Integer, ArrayList<Vector3fImmutable>> _buildingPatrolPoints = new HashMap<>();
public static int getAvailableSlot(Building building) {
ArrayList<BuildingLocation> slotLocations = _slotLocations.get(building.meshUUID);
// Some meshes might not have slot locations assigned.
if (slotLocations == null || slotLocations.isEmpty())
return -1;
int numOfSlots = _slotLocations.get(building.meshUUID).size();
for (int i = 1; i <= numOfSlots; i++) {
if (!building.getHirelings().containsValue(i))
return i;
}
return -1;
}
public static int getLastAvailableSlot(Building building) {
ArrayList<BuildingLocation> slotLocations = _slotLocations.get(building.meshUUID);
// Some meshes might not have slot locations assigned.
if (slotLocations == null || slotLocations.isEmpty())
return -1;
int numOfSlots = _slotLocations.get(building.meshUUID).size();
for (int i = numOfSlots; i > 0; i--)
if (!building.getHirelings().containsValue(i))
return i;
return -1;
}
public static BuildingLocation getSlotLocation(Building building, int slot) {
BuildingLocation buildingLocation = new BuildingLocation();
if (slot == -1)
return buildingLocation;
buildingLocation = _slotLocations.get(building.meshUUID).get(slot - 1); // array index
if (buildingLocation == null)
Logger.error("Invalid slot for building: " + building.getObjectUUID());
return buildingLocation;
}
public static boolean playerCanManage(PlayerCharacter player, Building building) {
if (player == null)
return false;
if (building == null)
return false;
//cannot access destroyed buildings
if (building.getRank() == -1)
return false;
//admin characters can always access buildings
if (player.isCSR())
return true;
//owner can always access their own building
if (IsOwner(building, player))
return true;
//check for default IC access if building belongs to same guild
if(player.guild.equals(building.getGuild())) {
if (building.getBlueprint() != null && building.getBlueprint().getBuildingGroup() != null) {
switch (building.getBlueprint().getBuildingGroup()) {
case TOL:
case BARRACK:
case SPIRE:
case SHRINE:
case BANESTONE:
case MINE:
case WAREHOUSE:
case BULWARK:
case SIEGETENT:
if (GuildStatusController.isInnerCouncil(player.getGuildStatus()))
return true;
if (GuildStatusController.isGuildLeader(player.getGuildStatus()))
return true;
break;
}
}
}
//check against friends list entries if any present
if (building.getFriends() != null) {
//check individuals
if (building.getFriends().get(player.getObjectUUID()) != null)
return true;
if (building.getFriends().get(player.guild.objectUUID) != null) {
//check friend type for guild related access
switch (building.getFriends().get(player.guild.objectUUID).friendType) {
case 8: //full member
if (GuildStatusController.isFullMember(player.getGuildStatus()))
return true;
break;
case 9: //Inner Council
if (GuildStatusController.isInnerCouncil(player.getGuildStatus()))
return true;
if (GuildStatusController.isGuildLeader(player.getGuildStatus()))
return true;
break;
}
}
}
//did not meet access grant criteria, deny access
return false;
}
public static synchronized boolean lootBuilding(PlayerCharacter player, Building building) {
if (building == null)
return false;
if (player == null)
return false;
if (building.getRank() != -1)
return false;
if (building.getBlueprintUUID() == 0)
return false;
switch (building.getBlueprint().getBuildingGroup()) {
case SHRINE:
Shrine shrine = Shrine.shrinesByBuildingUUID.get(building.getObjectUUID());
if (shrine == null)
return false;
int amount = shrine.getFavors();
//no more favors too loot!
if (amount == 0) {
try {
ErrorPopupMsg.sendErrorPopup(player, 166);
} catch (Exception e) {
}
return false;
}
ItemTemplate template = ItemTemplate.templates.get(1705032); // Elan Stone
if (!player.charItemManager.hasRoomInventory(template.item_wt))
return false;
if (!ItemManager.MakeItemForPlayer(template.template_id, player, amount))
return false;
shrine.setFavors(0);
break;
case WAREHOUSE:
Warehouse warehouse = Warehouse.destroyedWarehouses.get(building.getObjectUUID());
if (warehouse == null)
return false;
for (mbEnums.ResourceType resourceType : EnumSet.allOf(mbEnums.ResourceType.class)) {
ItemTemplate resourceTemplate = ItemTemplate.templates.get(resourceType.templateID);
if (resourceTemplate == null)
continue;
if (!player.charItemManager.hasRoomInventory(resourceTemplate.item_wt)) {
ChatManager.chatSystemInfo(player, "You can not carry any more of that item.");
return false;
}
if (warehouse.resources.get(resourceType) == null)
continue;
int resourceAmount = warehouse.resources.get(resourceType);
if (resourceAmount <= 0)
continue;
if (Warehouse.loot(warehouse, player, resourceType, resourceAmount, true)) {
ChatManager.chatInfoInfo(player, "You have looted " + resourceAmount + ' ' + resourceType.name());
}
}
if (Warehouse.isEmpty(warehouse))
Warehouse.destroyedWarehouses.remove(building.getObjectUUID());
break;
}
//Everything was looted, Maybe we should
return true;
}
//This method restarts an upgrade timer when a building is loaded from the database.
// Submit upgrade job for this building based upon it's current upgradeDateTime
public static void submitUpgradeJob(Building building) {
if (building == null)
return;
if (building.getUpgradeDateTime() == null) {
Logger.error("Attempt to submit upgrade job for non-ranking building");
return;
}
// Submit upgrade job for future date or current instant
if (building.getUpgradeDateTime().isAfter(LocalDateTime.now())) {
JobContainer jc = JobScheduler.getInstance().scheduleJob(new UpgradeBuildingJob(building), building.getUpgradeDateTime().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
} else
JobScheduler.getInstance().scheduleJob(new UpgradeBuildingJob(building), 0);
}
public static void setUpgradeDateTime(Building building, LocalDateTime upgradeDateTime, int rankCost) {
if (building == null)
return;
if (!DbManager.BuildingQueries.updateBuildingUpgradeTime(upgradeDateTime, building, rankCost)) {
Logger.error("Failed to set upgradeTime for building " + building.getObjectUUID());
return;
}
building.upgradeDateTime = upgradeDateTime;
}
// Method transfers ownership of all hirelings in a building
public static void refreshHirelings(Building building) {
if (building == null)
return;
Guild newGuild;
if (building.getOwner() == null)
newGuild = Guild.getErrantGuild();
else
newGuild = building.getOwner().getGuild();
for (AbstractCharacter hireling : building.getHirelings().keySet()) {
hireling.setGuild(newGuild);
WorldGrid.updateObject(hireling);
}
}
public static void removeHireling(Building building, AbstractCharacter hireling) {
if (hireling.getObjectType().equals(GameObjectType.Mob)) {
Mob guardCaptain = (Mob) hireling;
// Clear minions from database if a guard captain
if (guardCaptain.agentType.equals(mbEnums.AIAgentType.GUARDCAPTAIN))
DbManager.MobQueries.REMOVE_ALL_MINIONS(hireling.getObjectUUID());
}
// Clear minions from world
for (Integer minionUUID : hireling.minions) {
Mob minionMob = Mob.getMob(minionUUID);
DbManager.removeFromCache(minionMob);
WorldGrid.RemoveWorldObject(minionMob);
WorldGrid.unloadObject(minionMob);
if (minionMob.parentZone != null)
minionMob.parentZone.zoneMobSet.remove(minionMob);
}
// Clear all workorders for this hireling
if (hireling.getObjectType().equals(GameObjectType.NPC)) {
NPC hirelingNPC = (NPC) hireling;
if (ForgeManager.vendorWorkOrderLookup.get(hirelingNPC) != null)
for (WorkOrder workOrder : ForgeManager.vendorWorkOrderLookup.get(hirelingNPC)) {
workOrder.runCompleted.set(true);
workOrder.vendor = null;
// Remove any cooking items from collections
// to ensure we don't leak memory.
for (Item item : workOrder.cooking) {
DbManager.removeFromCache(item);
ForgeManager.itemWorkOrderLookup.remove(item);
}
DbManager.WarehouseQueries.DELETE_WORKORDER(workOrder);
}
// Finally remove the NPC from ForgeManager
ForgeManager.vendorWorkOrderLookup.remove(hirelingNPC);
}
// Remove hireling from building
building.getHirelings().remove(hireling);
// Remove from zone mob set
if (hireling.getObjectType().equals(GameObjectType.Mob)) {
Mob hirelingMob = (Mob) hireling;
if (hirelingMob.parentZone != null)
if (hirelingMob.parentZone.zoneMobSet.contains(hirelingMob))
hirelingMob.parentZone.zoneMobSet.remove(hireling);
}
if (hireling.getObjectType().equals(GameObjectType.NPC)) {
NPC hirelingNPC = (NPC) hireling;
if (hirelingNPC.getParentZone() != null)
if (hirelingNPC.getParentZone().zoneNPCSet.contains(hirelingNPC))
hirelingNPC.getParentZone().zoneNPCSet.remove(hireling);
}
// Unload hireling from world
DbManager.removeFromCache(hireling);
WorldGrid.RemoveWorldObject(hireling);
WorldGrid.removeObject(hireling);
// Delete hireling from database
if (hireling.getObjectType().equals(GameObjectType.Mob))
DbManager.MobQueries.DELETE_MOB((Mob) hireling);
else
DbManager.NPCQueries.DELETE_NPC((NPC) hireling);
}
public static void cleanupHirelings(Building building) {
// Early exit: Cannot have hirelings in a building
// without a blueprint.
if (building.getBlueprintUUID() == 0)
return;
// Remove all hirelings for destroyed buildings
if (building.getRank() < 1) {
for (AbstractCharacter slottedNPC : building.getHirelings().keySet())
BuildingManager.removeHireling(building, slottedNPC);
return;
}
// Delete hireling if building has deranked.
for (AbstractCharacter hireling : building.getHirelings().keySet())
if (building.getHirelings().get(hireling) > building.getBlueprint().getSlotsForRank(building.getRank()))
BuildingManager.removeHireling(building, hireling);
refreshHirelings(building);
}
public static Building getBuilding(int id) {
if (id == 0)
return null;
Building building;
building = (Building) DbManager.getFromCache(mbEnums.GameObjectType.Building, id);
if (building != null)
return building;
return DbManager.BuildingQueries.GET_BUILDINGBYUUID(id);
}
public static boolean PlayerCanControlNotOwner(Building building, PlayerCharacter player) {
if (player == null)
return false;
if (building == null)
return false;
if (building.getOwner() == null)
return false;
//lets pass true if player is owner anyway.
if (building.getOwner().equals(player))
return true;
if (player.getGuild().isEmptyGuild())
return false;
if (building.getGuild().isGuildLeader(player.getObjectUUID()))
return true;
if (!Guild.sameGuild(building.getGuild(), player.getGuild()))
return false;
return GuildStatusController.isGuildLeader(player.getGuildStatus()) || GuildStatusController.isInnerCouncil(player.getGuildStatus());
}
public static boolean IsPlayerHostile(Building building, PlayerCharacter player) {
if (Guild.sameNationExcludeErrant(building.getGuild(), player.getGuild()))
return false;
if(building.enforceKOS) {
if (building.getCity() != null) {
Building TOL = building.getCity().getTOL();
if (TOL != null) {
building = TOL;
}
}
}
if (!building.reverseKOS) {
Condemned condemn = building.getCondemned().get(player.getObjectUUID());
if (condemn != null && condemn.active)
return true;
if (player.getGuild() != null) {
Condemned guildCondemn = building.getCondemned().get(player.getGuild().getObjectUUID());
if (guildCondemn != null && guildCondemn.active)
return true;
Condemned nationCondemn = building.getCondemned().get(player.getGuild().getNation().getObjectUUID());
return nationCondemn != null && nationCondemn.active && nationCondemn.friendType == Condemned.NATION;
} else {
//TODO ADD ERRANT KOS CHECK
}
} else {
Condemned condemn = building.getCondemned().get(player.getObjectUUID());
if (condemn != null && condemn.active)
return false;
if (player.getGuild() != null) {
Condemned guildCondemn = building.getCondemned().get(player.getGuild().getObjectUUID());
if (guildCondemn != null && guildCondemn.active)
return false;
Condemned nationCondemn = building.getCondemned().get(player.getGuild().getNation().getObjectUUID());
return nationCondemn == null || !nationCondemn.active || nationCondemn.friendType != Condemned.NATION;
} else {
//TODO ADD ERRANT KOS CHECK
}
return true;
}
//When we get to here, This means The building was not reverse KOS
//and passed the hostile test.
return false;
}
public static final synchronized boolean addHirelingForWorld(Building building, PlayerCharacter contractOwner, Vector3fImmutable NpcLoc, Zone zone, Contract NpcID, int rank) {
String pirateName = NPCManager.getPirateName(NpcID.getMobbaseID());
NPC npc;
npc = NPC.createNPC(pirateName, NpcID.getObjectUUID(), NpcLoc, building.getGuild(), zone, (short) rank, building);
if (npc == null)
return false;
npc.setObjectTypeMask(MBServerStatics.MASK_NPC);
npc.setLoc(npc.bindLoc);
InterestManager.setObjectDirty(npc);
return true;
}
public static synchronized boolean addHireling(Building building, PlayerCharacter contractOwner, Zone zone, Contract contract, Item item) {
int rank;
if (building.getBlueprintUUID() == 0)
return false;
if (building.getBlueprint().getMaxSlots() == building.getHirelings().size())
return false;
String pirateName = NPCManager.getPirateName(contract.getMobbaseID());
if ((byte) item.chargesRemaining > 0)
rank = (byte) item.chargesRemaining * 10;
else
rank = 10;
Mob mobile;
NPC npc;
if (NPC.ISWallArcher(contract)) {
mobile = Mob.createMob(contract.getMobbaseID(), Vector3fImmutable.ZERO, contractOwner.getGuild(), zone, building, contract, pirateName, rank, mbEnums.AIAgentType.GUARDWALLARCHER);
if (mobile == null)
return false;
// Configure AI and write new mobile to disk
mobile.behaviourType = mbEnums.MobBehaviourType.GuardWallArcher;
mobile = DbManager.MobQueries.PERSIST(mobile);
// Spawn new mobile
mobile.setLoc(mobile.getLoc());
return true;
}
if (NPC.ISGuardCaptain(contract.getContractID())) {
mobile = Mob.createMob(contract.getMobbaseID(), Vector3fImmutable.ZERO, contractOwner.getGuild(), zone, building, contract, pirateName, rank, mbEnums.AIAgentType.GUARDCAPTAIN);
if (mobile == null)
return false;
// Configure AI and write new mobile to disk
mobile.behaviourType = mbEnums.MobBehaviourType.GuardCaptain;
mobile = DbManager.MobQueries.PERSIST(mobile);
// Spawn new mobile
mobile.setLoc(mobile.getLoc());
return true;
}
if (contract.getContractID() == 910) {
//guard dog
mobile = Mob.createMob(contract.getMobbaseID(), Vector3fImmutable.ZERO, contractOwner.getGuild(), zone, building, contract, pirateName, rank, mbEnums.AIAgentType.GUARDCAPTAIN);
if (mobile == null)
return false;
// Configure AI and write new mobile to disk
mobile.behaviourType = mbEnums.MobBehaviourType.GuardCaptain;
mobile = DbManager.MobQueries.PERSIST(mobile);
// Spawn new mobile
mobile.setLoc(mobile.getLoc());
return true;
}
npc = NPC.createNPC(pirateName, contract.getObjectUUID(), Vector3fImmutable.ZERO, contractOwner.getGuild(), zone, (short) rank, building);
if (npc == null)
return false;
npc.setObjectTypeMask(MBServerStatics.MASK_NPC);
npc.setLoc(npc.bindLoc);
InterestManager.setObjectDirty(npc);
return true;
}
public static boolean IsWallPiece(Building building) {
if (building.getBlueprint() == null)
return false;
BuildingGroup buildingGroup = building.getBlueprint().getBuildingGroup();
switch (buildingGroup) {
case WALLSTRAIGHT:
case WALLCORNER:
case SMALLGATE:
case ARTYTOWER:
case WALLSTRAIGHTTOWER:
case WALLSTAIRS:
return true;
default:
return false;
}
}
public static Building getBuildingFromCache(int id) {
return (Building) DbManager.getFromCache(GameObjectType.Building, id);
}
public static boolean IsOwner(Building building, PlayerCharacter player) {
if (building == null || player == null)
return false;
if (building.getOwner() == null)
return false;
return building.getOwner().getObjectUUID() == player.getObjectUUID();
}
public static float GetMissingHealth(Building building) {
return building.healthMax - building.getCurrentHitpoints();
}
public static int GetRepairCost(Building building) {
return (int) (GetMissingHealth(building) * .10f);
}
public static Regions GetRegion(Building building, float x, float y, float z) {
if (building.getBounds() == null)
return null;
if (building.getBounds().getRegions() == null)
return null;
Regions currentRegion = null;
for (Regions region : building.getBounds().getRegions()) {
if (region.isPointInPolygon(new Vector3fImmutable(x, y, z)))
if (y > (region.highLerp.y - 5))
currentRegion = region;
}
return currentRegion;
}
public static Regions GetRegion(Building building, int room, int level, float x, float z) {
if (building.getBounds() == null)
return null;
if (building.getBounds().getRegions() == null)
return null;
for (Regions region : building.getBounds().getRegions()) {
if (region.getLevel() != level)
continue;
if (region.getRoom() != room)
continue;
if (region.isPointInPolygon(new Vector3fImmutable(x, 0, z)))
return region;
}
return null;
}
public static Vector3fImmutable GetBindLocationForBuilding(Building building) {
Vector3fImmutable bindLoc = null;
if (building == null)
return mbEnums.Ruins.getRandomRuin().getLocation();
bindLoc = building.getLoc();
float radius = Bounds.meshBoundsCache.get(building.getMeshUUID()).radius;
if (building.getRank() == 8) {
bindLoc = building.getStuckLocation();
if (bindLoc != null)
return bindLoc;
}
float x = bindLoc.getX();
float z = bindLoc.getZ();
float offset = ((ThreadLocalRandom.current().nextFloat() * 2) - 1) * radius;
int direction = ThreadLocalRandom.current().nextInt(4);
switch (direction) {
case 0:
x += radius;
z += offset;
break;
case 1:
x += offset;
z -= radius;
break;
case 2:
x -= radius;
z += offset;
break;
case 3:
x += offset;
z += radius;
break;
}
bindLoc = new Vector3fImmutable(x, bindLoc.getY(), z);
return bindLoc;
}
public static void rebuildMine(Building mineBuilding) {
setRank(mineBuilding, 1);
mineBuilding.meshUUID = mineBuilding.getBlueprint().getMeshForRank(mineBuilding.rank);
// New rank mean new max hit points.
mineBuilding.healthMax = mineBuilding.getBlueprint().getMaxHealth(mineBuilding.rank);
mineBuilding.setCurrentHitPoints(mineBuilding.healthMax);
mineBuilding.getBounds().setBounds(mineBuilding);
}
public static void setRank(Building building, int rank) {
int newMeshUUID;
boolean success;
// If this building has no blueprint then set rank and exit immediatly.
if (building.blueprintUUID == 0 || building.getBlueprint() != null && building.getBlueprint().getBuildingGroup().equals(BuildingGroup.MINE)) {
building.rank = rank;
DbManager.BuildingQueries.CHANGE_RANK(building.getObjectUUID(), rank);
return;
}
// Delete any upgrade jobs before doing anything else. It won't quite work
// if in a few lines we happen to delete this building.
JobContainer jobContainer = building.getTimers().get("UPGRADE");
if (jobContainer != null) {
if (!JobScheduler.getInstance().cancelScheduledJob(jobContainer))
Logger.error("failed to cancel existing upgrade job.");
}
// Attempt to write to database or delete the building
// if we are destroying it.
if (rank == -1)
success = DbManager.BuildingQueries.DELETE_FROM_DATABASE(building);
else
success = DbManager.BuildingQueries.updateBuildingRank(building, rank);
if (!success) {
Logger.error("Error writing to database UUID: " + building.getObjectUUID());
return;
}
building.isDeranking.compareAndSet(false, true);
// Change the building's rank
building.rank = rank;
// New rank means new mesh
newMeshUUID = building.getBlueprint().getMeshForRank(building.rank);
if ((building.getBlueprint().getBuildingGroup() == BuildingGroup.TOL) && (building.rank == 8))
newMeshUUID = Realm.getRealmMesh(building.getCity());
building.meshUUID = newMeshUUID;
// New rank mean new max hitpoints.
building.healthMax = building.getBlueprint().getMaxHealth(building.rank);
building.setCurrentHitPoints(building.healthMax);
if (building.getUpgradeDateTime() != null)
setUpgradeDateTime(building, null, 0);
// If we destroyed this building make sure to turn off
// protection
if (building.rank == -1)
building.protectionState = mbEnums.ProtectionState.NONE;
// update object to clients
building.refresh(true);
if (building.getBounds() != null)
building.getBounds().setBounds(building);
// Cleanup hirelings resulting from rank change
cleanupHirelings(building);
building.isDeranking.compareAndSet(true, false);
}
public static Building getBuildingAtLocation(Vector3fImmutable loc) {
for (AbstractWorldObject awo : WorldGrid.getObjectsInRangePartial(loc, 64, MBServerStatics.MASK_BUILDING)) {
Building building = (Building) awo;
if (building == null)
continue;
if (Bounds.collide(loc, building.getBounds()))
return building;
}
return null;
}
}