// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ . // ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌· // ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀ // ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌ // ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀ // Magicbane Emulator Project © 2013 - 2026 // https://www.magicbane.com package engine.objects; import engine.InterestManagement.RealmMap; import engine.InterestManagement.Terrain; import engine.InterestManagement.WorldGrid; import engine.db.archive.CityRecord; import engine.db.archive.DataWarehouse; import engine.db.archive.MineRecord; import engine.gameManager.BuildingManager; import engine.gameManager.DbManager; import engine.gameManager.DispatchManager; import engine.gameManager.ZoneManager; import engine.job.JobContainer; import engine.job.JobScheduler; import engine.jobs.DoorCloseJob; import engine.jobs.SiegeSpireWithdrawlJob; import engine.math.Bounds; import engine.math.Quaternion; import engine.math.Vector3f; import engine.math.Vector3fImmutable; import engine.mbEnums; import engine.mbEnums.*; import engine.net.ByteBufferWriter; import engine.net.client.msg.ApplyBuildingEffectMsg; import engine.net.client.msg.UpdateObjectMsg; import engine.server.MBServerStatics; import org.joda.time.DateTime; import org.pmw.tinylog.Logger; import java.sql.ResultSet; import java.sql.SQLException; import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneId; import java.util.ArrayList; import java.util.HashMap; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.locks.ReentrantReadWriteLock; public class Building extends AbstractWorldObject { // Used for thread safety private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); private final ConcurrentHashMap hirelings = new ConcurrentHashMap<>(MBServerStatics.CHM_INIT_CAP, MBServerStatics.CHM_LOAD, MBServerStatics.CHM_THREAD_LOW); private final HashMap doorJobs = new HashMap<>(); public int meshUUID; public Zone parentZone; public int parentZoneUUID; public boolean reverseKOS; public int reserve = 0; public float statLat; public float statLon; public float statAlt; public LocalDateTime upgradeDateTime = null; public LocalDateTime taxDateTime = null; public ArrayList sentryPoints = new ArrayList<>(); public TaxType taxType = TaxType.NONE; public int taxAmount; public boolean enforceKOS = false; // Variables NOT to be stored in db public int parentBuildingID; public boolean isFurniture = false; public int floor; public int level; public AtomicBoolean isDeranking = new AtomicBoolean(false); public LocalDateTime maintDateTime; /* The Blueprint class has methods able to derive * all defining characteristics of this building, */ public int blueprintUUID = 0; public int rank; public ArrayList patrolPoints; public ProtectionState protectionState = ProtectionState.NONE; protected Resists resists; private Vector3f meshScale = new Vector3f(1.0f, 1.0f, 1.0f); private int doorState = 0; private int ownerUUID = 0; //NPC or Character--check ownerIsNPC flag private int _strongboxValue = 0; private int maxGold; private int effectFlags = 0; private String name = ""; private boolean ownerIsNPC = true; private boolean spireIsActive = false; private ConcurrentHashMap timers = null; private ConcurrentHashMap timestamps = null; private ConcurrentHashMap friends; private ConcurrentHashMap condemned; private ArrayList children = null; /** * No Id Constructor */ public Building() { super(); } /** * ResultSet Constructor */ public Building(ResultSet rs) throws SQLException { super(rs); float scale; try { this.meshUUID = rs.getInt("meshUUID"); this.setObjectTypeMask(MBServerStatics.MASK_BUILDING); this.blueprintUUID = rs.getInt("blueprintUUID"); this.gridObjectType = GridObjectType.STATIC; this.parentZoneUUID = rs.getInt("parent"); this.name = rs.getString("name"); this.ownerUUID = rs.getInt("ownerUUID"); this.doorState = rs.getInt("doorState"); this.setHealth(rs.getInt("currentHP")); this.setRotation(new Quaternion(0f, rs.getFloat("rotY"), 0f, rs.getFloat("w"))); this.reverseKOS = rs.getByte("reverseKOS") == 1; this.statLat = rs.getFloat("locationX"); this.statAlt = rs.getFloat("locationY"); this.statLon = rs.getFloat("locationZ"); scale = rs.getFloat("scale"); this.meshScale = new Vector3f(scale, scale, scale); this.rank = rs.getInt("rank"); this.parentBuildingID = rs.getInt("parentBuildingID"); //create a new list if the building is a parent and not a child. if (this.parentBuildingID == 0) this.children = new ArrayList<>(); this.floor = rs.getInt("floor"); this.level = rs.getInt("level"); this.isFurniture = (rs.getBoolean("isFurniture")); this._strongboxValue = rs.getInt("currentGold"); this.maxGold = 15000000; // *** Refactor to blueprint method this.reserve = rs.getInt("reserve"); this.taxType = TaxType.valueOf(rs.getString("taxType")); this.taxAmount = rs.getInt("taxAmount"); this.protectionState = ProtectionState.valueOf(rs.getString("protectionState")); java.sql.Timestamp maintTimeStamp = rs.getTimestamp("maintDate"); if (maintTimeStamp != null) this.maintDateTime = LocalDateTime.ofInstant(maintTimeStamp.toInstant(), ZoneId.systemDefault()); java.sql.Timestamp taxTimeStamp = rs.getTimestamp("taxDate"); if (taxTimeStamp != null) this.taxDateTime = LocalDateTime.ofInstant(taxTimeStamp.toInstant(), ZoneId.systemDefault()); java.sql.Timestamp upgradeTimeStamp = rs.getTimestamp("upgradeDate"); if (upgradeTimeStamp != null) this.upgradeDateTime = LocalDateTime.ofInstant(upgradeTimeStamp.toInstant(), ZoneId.systemDefault()); if(rs.getInt("enforceKOS") == 0) this.enforceKOS = false; else this.enforceKOS = true; } catch (Exception e) { Logger.error("Failed for object " + this.blueprintUUID + ' ' + this.getObjectUUID() + e); } } // Defaults mirror the obj_building column defaults so a new building matches a reloaded one. public static Building createBuilding(Zone parentZone, int ownerUUID, String name, int meshUUID, Vector3fImmutable localLoc, float scale, int currentHP, ProtectionState protectionState, int currentGold, int rank, DateTime upgradeDate, int blueprintUUID, float w, float rotY) { Building building = new Building(); building.setObjectTypeMask(MBServerStatics.MASK_BUILDING); building.gridObjectType = GridObjectType.STATIC; building.parentZoneUUID = parentZone.getObjectUUID(); building.ownerUUID = ownerUUID; building.name = name; building.meshUUID = meshUUID; building.blueprintUUID = blueprintUUID; building.statLat = localLoc.x; building.statAlt = localLoc.y; building.statLon = localLoc.z; building.meshScale = new Vector3f(scale, scale, scale); building.setHealth(currentHP); building.protectionState = protectionState; building._strongboxValue = currentGold; building.maxGold = 15000000; building.rank = rank; building.setRotation(new Quaternion(0f, rotY, 0f, w)); building.parentBuildingID = 0; building.floor = -1; building.level = -1; if (upgradeDate != null) building.upgradeDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(upgradeDate.getMillis()), ZoneId.systemDefault()); return building; } // Writes a new player building once and loads it into the simulation; returns null if the insert failed. public static Building persistBuilding(Building building) { int objectUUID; objectUUID = DbManager.BuildingQueries.PERSIST(building); if (objectUUID <= 0) return null; building.setObjectUUID(objectUUID); DbManager.addToCache(building); building.runAfterLoad(); return building; } /* * Getters */ public static void _serializeForClientMsg(Building building, ByteBufferWriter writer) { writer.putInt(building.getObjectType().ordinal()); writer.putInt(building.getObjectUUID()); writer.putInt(0); // pad writer.putInt(building.meshUUID); writer.putInt(0); // pad if (building.parentBuildingID != 0) { writer.putFloat(building.statLat); writer.putFloat(building.statAlt); writer.putFloat(building.statLon); } else { writer.putFloat(building.getLoc().getX()); writer.putFloat(building.getLoc().getY()); // Y location writer.putFloat(building.getLoc().getZ()); } writer.putFloat(building.getRotation().w); writer.putFloat(building.getRotation().x); writer.putFloat(building.getRotation().y); writer.putFloat(building.getRotation().z); writer.putFloat(building.meshScale.getX()); writer.putFloat(building.meshScale.getY()); writer.putFloat(building.meshScale.getZ()); if (building.parentBuildingID != 0) { writer.putInt(GameObjectType.Building.ordinal()); writer.putInt(building.parentBuildingID); writer.putInt(building.floor); writer.putInt(building.level); } else { writer.putInt(0); // Pad //Parent writer.putInt(0); // Pad writer.putInt(-1); // Static writer.putInt(-1); // Static } writer.put((byte) 0); // 0 writer.putFloat(3); // 3 writer.putInt(GameObjectType.Building.ordinal()); writer.putInt(building.getObjectUUID()); if (building.ownerIsNPC) writer.putInt(GameObjectType.NPC.ordinal()); else writer.putInt(GameObjectType.PlayerCharacter.ordinal()); writer.putInt(building.ownerUUID); writer.put((byte) 1); // End Datablock writer.putFloat(building.health.get()); writer.putFloat(building.healthMax); if (building.blueprintUUID == 0) writer.putInt(0); else writer.putInt(building.getBlueprint().getIcon()); writer.putInt(building.effectFlags); writer.put((byte) 1); // End Datablock Guild g = building.getGuild(); Guild nation = null; if (g == null) { for (int i = 0; i < 3; i++) { writer.putInt(16); } for (int i = 0; i < 2; i++) { writer.putInt(0); } } else { GuildTag._serializeForDisplay(g.getGuildTag(), writer); nation = g.getNation(); } writer.put((byte) 1); // End Datablock? if (nation == null) { for (int i = 0; i < 3; i++) { writer.putInt(16); } for (int i = 0; i < 2; i++) { writer.putInt(0); } } else GuildTag._serializeForDisplay(nation.getGuildTag(), writer); writer.putString(building.name); writer.put((byte) 0); // End datablock } public final boolean isRanking() { return this.upgradeDateTime != null; } public final int getRank() { return rank; } public final int getOwnerUUID() { return ownerUUID; } public final boolean isOwnerIsNPC() { return ownerIsNPC; } public final City getCity() { if (this.parentZone == null) return null; if (this.getBlueprint() != null && this.getBlueprint().isSiegeEquip() && this.protectionState.equals(ProtectionState.PROTECTED)) { if (this.getGuild() != null) { if (this.getGuild().getOwnedCity() != null) { if (this.getLoc().isInsideCircle(this.getGuild().getOwnedCity().getLoc(), CityBoundsType.ZONE.halfExtents)) return this.getGuild().getOwnedCity(); } else { Bane bane = Bane.getBaneByAttackerGuild(this.getGuild()); if (bane != null) { if (bane.getCity() != null) { if (this.getLoc().isInsideCircle(bane.getCity().getLoc(), CityBoundsType.ZONE.halfExtents)) return bane.getCity(); } } } } } if (this.parentZone.guild_zone == false) return null; return City.getCity(this.parentZone.playerCityUUID); } public final String getCityName() { City city = getCity(); if (city != null) return city.getName(); return ""; } public final Blueprint getBlueprint() { if (this.blueprintUUID == 0) return null; return Blueprint.getBlueprint(this.blueprintUUID); } public final int getBlueprintUUID() { return this.blueprintUUID; } public final void setCurrentHitPoints(Float CurrentHitPoints) { this.addDatabaseJob("health", MBServerStatics.ONE_MINUTE); this.setHealth(CurrentHitPoints); } //This method is to handle when a building is damaged below 0 health. //Either destroy or derank it. public final LocalDateTime getUpgradeDateTime() { lock.readLock().lock(); try { return upgradeDateTime; } finally { lock.readLock().unlock(); } } public final float modifyHealth(final float value, final AbstractCharacter attacker) { if (this.rank == -1) return 0f; boolean worked = false; Float oldHealth = 0f, newHealth = 0f; while (!worked) { if (this.rank == -1) return 0f; oldHealth = this.health.get(); newHealth = oldHealth + value; if (newHealth > this.healthMax) newHealth = healthMax; worked = this.health.compareAndSet(oldHealth, newHealth); } if (newHealth < 0) { if (this.isDeranking.compareAndSet(false, true)) { this.destroyOrDerank(attacker); } return newHealth - oldHealth; } else this.addDatabaseJob("health", MBServerStatics.ONE_MINUTE); if (value < 0) Mine.SendMineAttackMessage(this); City playerCity = ZoneManager.getCityAtLocation(this.loc); if (playerCity != null) { if (this.getGuild().getNation().equals(playerCity.getTOL().getGuild().getNation())) { //friendly building has been attacked, add attacker to city outlaw list if (!playerCity.cityOutlaws.contains(attacker.getObjectUUID()) && attacker.getObjectType().equals(GameObjectType.PlayerCharacter)) playerCity.cityOutlaws.add(attacker.getObjectUUID()); //only city guards respond; siege engines and pets in the zone are left alone for (Mob guard : playerCity.getParent().zoneMobSet) if (guard.isPlayerGuard() && guard.combatTarget == null) guard.setCombatTarget(attacker); } } return newHealth - oldHealth; } public final void destroyOrDerank(AbstractCharacter attacker) { Blueprint blueprint; City city; // Sanity check: Early exit if a non // blueprinted object is attempting to // derank. if (this.blueprintUUID == 0) return; blueprint = this.getBlueprint(); city = this.getCity(); // Special handling of destroyed Banes if (blueprint.getBuildingGroup() == BuildingGroup.BANESTONE) { city.getBane().endBane(SiegeResult.DEFEND); return; } // Special handling of warehouses if (blueprint.getBuildingGroup() == BuildingGroup.WAREHOUSE) { Warehouse.destroyedWarehouses.put(this.getObjectUUID(), city.warehouse); DbManager.WarehouseQueries.DELETE_WAREHOUSE(city.warehouse); city.warehouse = null; } // Special handling of destroyed Spires if ((blueprint.getBuildingGroup() == BuildingGroup.SPIRE) && this.rank == 1) this.disableSpire(true); // Special handling of destroyed Mines if (blueprint.getBuildingGroup() == BuildingGroup.MINE && this.rank == 1) { Mine mine = Mine.getMineFromTower(this.getObjectUUID()); if (mine != null) { // Warehouse mine destruction event MineRecord mineRecord = MineRecord.borrow(mine, attacker, RecordEventType.DESTROY); DataWarehouse.pushToWarehouse(mineRecord); BuildingManager.setRank(this, -1); this.setCurrentHitPoints((float) 1); this.healthMax = (float) 1; this.meshUUID = this.getBlueprint().getMeshForRank(this.rank); mine.handleDestroyMine(); this.getBounds().setBounds(this); this.refresh(true); return; } } // Special handling of deranking Trees if (blueprint.getBuildingGroup() == BuildingGroup.TOL) { derankTreeOfLife(); return; } // If codepath reaches here then it's a regular // structure not requiring special handling. // Time to either derank or destroy the building. if ((this.rank - 1) < 1) BuildingManager.setRank(this, -1); else BuildingManager.setRank(this, this.rank - 1); } // Return the maint cost in gold associated with this structure private void derankTreeOfLife() { City city; Bane bane; Realm cityRealm; ArrayList spireBuildings = new ArrayList<>(); ArrayList shrineBuildings = new ArrayList<>(); ArrayList barracksBuildings = new ArrayList<>(); Building spireBuilding; Building shrineBuilding; SiegeResult siegeResult; AbstractCharacter newOwner; city = this.getCity(); if (city == null) { Logger.error("No city for tree of uuid" + this.getObjectUUID()); return; } bane = city.getBane(); // We need to collect the spires and shrines on the citygrid in case // they will be deleted as excess as the tree deranks. for (Building building : city.getParent().zoneBuildingSet) { //don't add -1 rank buildings. if (building.rank <= 0) continue; if (building.getBlueprint() != null && building.getBlueprint().getBuildingGroup() == BuildingGroup.SPIRE) spireBuildings.add(building); if (building.getBlueprint() != null && building.getBlueprint().getBuildingGroup() == BuildingGroup.SHRINE) shrineBuildings.add(building); if (building.getBlueprint() != null && building.getBlueprint().getBuildingGroup() == BuildingGroup.BARRACK) barracksBuildings.add(building); } // A tree can only hold so many spires. As it deranks we need to delete // the excess if (spireBuildings.size() > Blueprint.getMaxShrines(this.rank - 1)) { spireBuilding = spireBuildings.get(0); // Disable and delete a random spire if (spireBuilding != null) { spireBuilding.disableSpire(true); BuildingManager.setRank(spireBuilding, -1); } } if (shrineBuildings.size() > Blueprint.getMaxShrines(this.rank - 1)) { shrineBuilding = shrineBuildings.get(0); // Delete a random shrine if (shrineBuilding != null) BuildingManager.setRank(shrineBuilding, -1); } if (barracksBuildings.size() > this.rank - 1) { Building barracksBuilding = barracksBuildings.get(0); // Delete a random barrack if (barracksBuilding != null) BuildingManager.setRank(barracksBuilding, -1); } // If the tree is R8 and deranking, we need to update it's // mesh along with buildings losing their health bonus if (this.rank == 8) { cityRealm = city.realm; if (cityRealm != null) cityRealm.abandonRealm(); for (Building cityBuilding : this.parentZone.zoneBuildingSet) { if ((cityBuilding.getBlueprint() != null && cityBuilding.getBlueprint().getBuildingGroup() != BuildingGroup.TOL) && (cityBuilding.getBlueprint().getBuildingGroup() != BuildingGroup.BANESTONE)) { cityBuilding.healthMax = cityBuilding.getBlueprint().getMaxHealth(cityBuilding.rank); } if (cityBuilding.health.get() > cityBuilding.healthMax) cityBuilding.setHealth(cityBuilding.healthMax); } } // Tree is simply deranking. // Let's do so and early exit if (this.rank > 1) { BuildingManager.setRank(this, rank - 1); City.lastCityUpdate = System.currentTimeMillis(); return; } // Must remove a bane before considering destruction of a TOL if (bane != null) { // Cache the new owner newOwner = Guild.GetGL(bane.getOwner().getGuild()); this.isDeranking.compareAndSet(false, true); if (bane.getOwner().getGuild().guildState == GuildState.Sworn) siegeResult = SiegeResult.CAPTURE; else siegeResult = SiegeResult.DESTROY; // Remove realm if city had one Realm realm = RealmMap.getRealmAtLocation(city.getLoc()); if (realm != null) if (realm.isRealmFullAfterBane()) siegeResult = SiegeResult.DESTROY; city.getBane().endBane(siegeResult); // If it's a capture bane transfer the tree and exit if (siegeResult.equals(SiegeResult.CAPTURE)) { city.transfer(newOwner); CityRecord cityRecord = CityRecord.borrow(city, RecordEventType.CAPTURE); DataWarehouse.pushToWarehouse(cityRecord); return; } } // end removal of bane // if codepath reaches here then we can now destroy the tree and the city CityRecord cityRecord = CityRecord.borrow(city, RecordEventType.DESTROY); DataWarehouse.pushToWarehouse(cityRecord); city.destroy(); } public float getCurrentHitpoints() { return this.health.get(); } public int getMaintCost() { int maintCost = 0; // Add cost for building structure maintCost += this.getBlueprint().getMaintCost(rank); // Add costs associated with hirelings for (AbstractCharacter npc : this.hirelings.keySet()) { if (npc.getObjectType() != GameObjectType.NPC) continue; maintCost += Blueprint.getNpcMaintCost(npc.getRank()); } return maintCost; } public final void submitOpenDoorJob(int doorID) { //cancel any outstanding door close jobs for this door if (this.doorJobs.containsKey(doorID)) { this.doorJobs.get(doorID).cancelJob(); this.doorJobs.remove(doorID); } //add new door close job DoorCloseJob dcj = new DoorCloseJob(this, doorID); this.doorJobs.put(doorID, dcj); JobScheduler.getInstance().scheduleJob(dcj, MBServerStatics.DOOR_CLOSE_TIMER); } public final float getMaxHitPoints() { return this.healthMax; } public final void setMaxHitPoints(float maxHealth) { this.healthMax = maxHealth; } public final Vector3f getMeshScale() { return this.meshScale; } public final int getMeshUUID() { return this.meshUUID; } public final Resists getResists() { return this.resists; } public final Zone getParentZone() { return this.parentZone; } //Sets the relative position to a parent zone public final void setParentZone(Zone zone) { //update ZoneManager's zone building list if (zone != null) if (this.parentZone != null) { this.parentZone.zoneBuildingSet.remove(this); if (this.getBlueprint() != null && this.getBlueprint().getBuildingGroup().equals(BuildingGroup.BARRACK)) { this.RemoveFromBarracksList(); } zone.zoneBuildingSet.add(this); } else zone.zoneBuildingSet.add(this); else if (this.parentZone != null) { this.parentZone.zoneBuildingSet.remove(this); if (this.getBlueprint() != null && this.getBlueprint().getBuildingGroup().equals(BuildingGroup.BARRACK)) { this.RemoveFromBarracksList(); } } if (this.parentZone == null) { this.parentZone = zone; this.setLoc(new Vector3fImmutable(this.statLat + zone.absX, this.statAlt + zone.absY, this.statLon + zone.absZ)); } else this.parentZone = zone; if (this.getBlueprint() != null && this.getBlueprint().getBuildingGroup().equals(BuildingGroup.BARRACK)) { AddToBarracksList(); } } public final int getParentZoneID() { if (this.parentZone == null) return 0; return this.parentZone.getObjectUUID(); } public float getStatLat() { return statLat; } public float getStatLon() { return statLon; } public float getStatAlt() { return statAlt; } public Guild getGuild() { AbstractCharacter buildingOwner; buildingOwner = this.getOwner(); if (buildingOwner != null) return buildingOwner.getGuild(); else return Guild.getErrantGuild(); } public int getEffectFlags() { return this.effectFlags; } public void addEffectBit(int bit) { this.effectFlags |= bit; } public void removeAllVisualEffects() { this.effectFlags = 0; ApplyBuildingEffectMsg applyBuildingEffectMsg = new ApplyBuildingEffectMsg(3276859, 1, this.getObjectType().ordinal(), this.getObjectUUID(), 0); DispatchManager.sendToAllInRange(this, applyBuildingEffectMsg); } public void removeEffectBit(int bit) { this.effectFlags &= (~bit); } @Override public String getName() { return this.name; } public final void setName(String value) { if (DbManager.BuildingQueries.CHANGE_NAME(this, value) == false) return; this.name = value; this.updateName(); } public final AbstractCharacter getOwner() { if (this.ownerUUID == 0) return null; if (this.ownerIsNPC) return NPC.getNPC(this.ownerUUID); return PlayerCharacter.getPlayerCharacter(this.ownerUUID); } public final String getOwnerName() { AbstractCharacter owner = this.getOwner(); if (owner != null) return owner.getName(); return ""; } public final String getGuildName() { Guild g = getGuild(); if (g != null) return g.getName(); return "None"; } @Override public void updateDatabase() { // *** Refactor : Log error here to see if it's ever called } public final LocalDateTime getDateToUpgrade() { return upgradeDateTime; } public final boolean setStrongboxValue(int newValue) { boolean success = true; try { DbManager.BuildingQueries.SET_PROPERTY(this, "currentGold", newValue); this._strongboxValue = newValue; } catch (Exception e) { success = false; Logger.error("Error writing to database"); } return success; } public final int getStrongboxValue() { return _strongboxValue; } public final void refresh(boolean newMesh) { if (newMesh) WorldGrid.updateObject(this); else { UpdateObjectMsg uom = new UpdateObjectMsg(this, 3); DispatchManager.sendToAllInRange(this, uom); } } public final void updateName() { UpdateObjectMsg uom = new UpdateObjectMsg(this, 2); DispatchManager.sendToAllInRange(this, uom); } public final void refreshGuild() { UpdateObjectMsg uom = new UpdateObjectMsg(this, 5); DispatchManager.sendToAllInRange(this, uom); } public int getMaxGold() { return maxGold; } @Override public void runAfterLoad() { // Set Parent Zone this.parentZone = ZoneManager.getZoneByUUID(this.parentZoneUUID); this.parentZone.zoneBuildingSet.add(this); // Lookup building blueprint Blueprint blueprint; if (this.blueprintUUID == 0) blueprint = Blueprint._meshLookup.get(meshUUID); else blueprint = this.getBlueprint(); // Log error if something went horrible wrong if ((this.blueprintUUID != 0) && (blueprint == null)) Logger.error("Invalid blueprint for object: " + this.getObjectUUID()); // Note: We handle R8 tree edge case for mesh and health // after city is loaded to avoid recursive result set call // in City resulting in a stack ovreflow. if (blueprint != null) { // Only switch mesh for player dropped structures if (this.blueprintUUID != 0) this.meshUUID = blueprint.getMeshForRank(rank); this.healthMax = blueprint.getMaxHealth(this.rank); // If this object has no blueprint but is a blueprint // mesh then set it's current health to max health if (this.blueprintUUID == 0) this.setHealth(healthMax); this.patrolPoints = BuildingManager._buildingPatrolPoints.computeIfAbsent(this.getObjectUUID(), k -> new ArrayList<>()); if (this.patrolPoints == null) Logger.error("Null patrol points"); } else { this.healthMax = 100000; // Structures with no blueprint mesh this.setHealth(healthMax); } resists = new Resists("Building"); if (this.parentZone != null) { if (this.parentBuildingID != 0) { Building parentBuilding = BuildingManager.getBuilding(this.parentBuildingID); if (parentBuilding != null) { this.setLoc(new Vector3fImmutable(this.statLat + this.parentZone.absX + parentBuilding.statLat, this.statAlt + this.parentZone.absY + parentBuilding.statAlt, this.statLon + this.parentZone.absZ + parentBuilding.statLon)); } else { this.setLoc(new Vector3fImmutable(this.statLat + this.parentZone.absX, this.statAlt + this.parentZone.absY, this.statLon + this.parentZone.absZ)); } } else { // Altitude of this building is derived from the heightmap engine. Vector3fImmutable tempLoc = new Vector3fImmutable(this.statLat + this.parentZone.absX, 0, this.statLon + this.parentZone.absZ); tempLoc = new Vector3fImmutable(tempLoc.x, Terrain.getWorldHeight(tempLoc) + this.statAlt, tempLoc.z); this.setLoc(tempLoc); } } // City load reads ownerIsNPC before NPCs are cached, so boot uses the owner set read ahead of buildings. this.ownerIsNPC = BuildingManager._npcBuildingOwners.contains(this.ownerUUID) || DbManager.inCache(GameObjectType.NPC, this.ownerUUID); // Reference friend and condemn lists from BuildingManager this.friends = BuildingManager._buildingFriends.computeIfAbsent(this.getObjectUUID(), k -> new ConcurrentHashMap<>()); this.condemned = BuildingManager._buildingCondemned.computeIfAbsent(this.getObjectUUID(), k -> new ConcurrentHashMap<>()); // Set bounds for this building Bounds buildingBounds = Bounds.borrow(); buildingBounds.setBounds(this); this.setBounds(buildingBounds); //create a new list for children if the building is not a child. children list default is null. //TODO Remove Furniture/Child buildings from building class and move them into a seperate class. if (this.parentBuildingID == 0) this.children = new ArrayList<>(); if (this.parentBuildingID != 0) { Building parent = BuildingManager.getBuilding(this.parentBuildingID); if (parent != null) { parent.children.add(this); //add furniture to region cache. floor and level are reversed in database, //TODO Fix Regions region = BuildingManager.GetRegion(parent, this.level, this.floor, this.getLoc().x, this.getLoc().z); if (region != null) Regions.FurnitureRegionMap.put(this.getObjectUUID(), region); } } if (this.upgradeDateTime != null) BuildingManager.submitUpgradeJob(this); } public synchronized boolean setOwner(AbstractCharacter newOwner) { int newOwnerID; if (newOwner == null) newOwnerID = 0; else newOwnerID = newOwner.getObjectUUID(); try { // Save new owner to database if (!DbManager.BuildingQueries.updateBuildingOwner(this, newOwnerID)) return false; if (newOwner == null) { this.ownerIsNPC = false; this.ownerUUID = 0; } else { this.ownerUUID = newOwner.getObjectUUID(); this.ownerIsNPC = (newOwner.getObjectType() == GameObjectType.NPC); } // Set new guild for hirelings and refresh all clients this.refreshGuild(); BuildingManager.refreshHirelings(this); } catch (Exception e) { Logger.error("Error updating owner! UUID: " + this.getObjectUUID()); return false; } return true; } //This turns on and off low damage effect for building public void toggleDamageLow(boolean on) { if (on) addEffectBit(2); else removeEffectBit(2); } //This turns on and off medium damage effect for building public void toggleDamageMedium(boolean on) { if (on) addEffectBit(4); else removeEffectBit(4); } //This turns on and off high damage effect for building public void toggleDamageHigh(boolean on) { if (on) addEffectBit(8); else removeEffectBit(8); } //This clears all damage effects on a building public void clearDamageEffect() { toggleDamageLow(false); toggleDamageMedium(false); toggleDamageHigh(false); } public Vector3fImmutable getStuckLocation() { Vector3fImmutable stuckLocation; ArrayList stuckLocations; stuckLocations = BuildingManager._stuckLocations.get(this.meshUUID); // Sanity check if (stuckLocations == null || stuckLocations.isEmpty()) return this.getLoc(); stuckLocation = stuckLocations.get(ThreadLocalRandom.current().nextInt(stuckLocations.size())).getLocation(); stuckLocation = this.getLoc().add(stuckLocation); // Rotate stuck position by the building rotation stuckLocation = Vector3fImmutable.rotateAroundPoint(this.getLoc(), stuckLocation, this.getBounds().getQuaternion().angleY); return stuckLocation; } public boolean isDoorOpen(int doorNumber) { if (this.doorState == 0) return false; return (this.doorState & (1 << doorNumber + 16)) != 0; } public boolean isDoorLocked(int doorNumber) { if (this.doorState == 0) return false; return (this.doorState & (1 << doorNumber)) != 0; } public boolean setDoorState(int doorNumber, DoorState doorState) { boolean updateRecord; updateRecord = false; // Can't have an invalid door number // Log error? if (doorNumber < 1 || doorNumber > 16) return false; switch (doorState) { case OPEN: this.doorState |= (1 << (doorNumber + 16)); break; case CLOSED: this.doorState &= ~(1 << (doorNumber + 16)); break; case UNLOCKED: this.doorState &= ~(1 << doorNumber); updateRecord = true; break; case LOCKED: this.doorState |= (1 << doorNumber); updateRecord = true; break; } // Save to database ? if (updateRecord) return DbManager.BuildingQueries.UPDATE_DOOR_LOCK(this.getObjectUUID(), this.doorState); else return true; } public void updateEffects() { ApplyBuildingEffectMsg applyBuildingEffectMsg = new ApplyBuildingEffectMsg(0x00720063, 1, this.getObjectType().ordinal(), this.getObjectUUID(), this.effectFlags); DispatchManager.sendToAllInRange(this, applyBuildingEffectMsg); } public final void enableSpire() { SpireType spireType; if (this.getCity() == null) return; // Blueprint sanity check if (this.blueprintUUID == 0) return; spireType = SpireType.getByBlueprintUUID(this.blueprintUUID); SiegeSpireWithdrawlJob spireJob = new SiegeSpireWithdrawlJob(this); JobContainer jc = JobScheduler.getInstance().scheduleJob(spireJob, 300000); this.getTimers().put("SpireWithdrawl", jc); this.getCity().addCityEffect(spireType.getEffectBase(), rank); addEffectBit(spireType.getEffectFlag()); this.spireIsActive = true; this.updateEffects(); } public final void disableSpire(boolean refreshEffect) { SpireType spireType; if (this.getCity() == null) return; // Blueprint sanity check if (this.blueprintUUID == 0) return; spireType = SpireType.getByBlueprintUUID(this.blueprintUUID); this.getCity().removeCityEffect(spireType.getEffectBase(), rank, refreshEffect); JobContainer toRemove = this.getTimers().get("SpireWithdrawl"); if (toRemove != null) { toRemove.cancelJob(); this.getTimers().remove("SpireWithdrawl"); } this.spireIsActive = false; this.removeEffectBit(spireType.getEffectFlag()); this.updateEffects(); } public ConcurrentHashMap getHirelings() { return hirelings; } public final boolean isSpireIsActive() { return spireIsActive; } public final void setSpireIsActive(boolean spireIsActive) { this.spireIsActive = spireIsActive; } public final boolean isVulnerable() { // NPC owned buildings are never vulnerable if (ownerIsNPC) return false; // Buildings on an npc citygrid are never vulnerable if (this.getCity() != null) { if (this.getCity().getParent().isNPCCity == true) return false; } // Destroyed buildings are never vulnerable if (rank < 0) return false; // Any structure without a blueprint was not placed by a // player and we can assume to be invulnerable regardless // of a protection contract or not. if (this.getBlueprint() == null) return false; // Runegates are never vulerable. if (this.getBlueprint().getBuildingGroup() == BuildingGroup.RUNEGATE) return false; // Shrines are never vulerable. They blow up as a // tree deranks. if (this.getBlueprint().getBuildingGroup() == BuildingGroup.SHRINE) return false; // Mines are vulnerable only if they are active if (this.getBlueprint().getBuildingGroup() == BuildingGroup.MINE) { // Cannot access mine if (Mine.getMineFromTower(this.getObjectUUID()) == null) return false; return Mine.getMineFromTower(this.getObjectUUID()).getIsActive() == true; } // Errant banestones are vulnerable by default if ((this.getBlueprint().getBuildingGroup() == BuildingGroup.BANESTONE) && this.getCity().getBane().isErrant() == true) return true; // There is an active protection contract. Is there also // an active bane? If so, it's meaningless. if (this.assetIsProtected() == true) { // Building protection is meaningless without a city if (this.getCity() == null) return true; // All buildings are vulnerable during an active bane return (this.getCity().protectionEnforced == false); } // No protection contract? Oh well, you're vunerable! return true; } public final ConcurrentHashMap getTimers() { if (this.timers == null) this.timers = new ConcurrentHashMap<>(MBServerStatics.CHM_INIT_CAP, MBServerStatics.CHM_LOAD, MBServerStatics.CHM_THREAD_LOW); return this.timers; } public final ConcurrentHashMap getTimestamps() { if (this.timestamps == null) this.timestamps = new ConcurrentHashMap<>(MBServerStatics.CHM_INIT_CAP, MBServerStatics.CHM_LOAD, MBServerStatics.CHM_THREAD_LOW); return this.timestamps; } public final long getTimeStamp(final String name) { if (this.getTimestamps().containsKey(name)) return this.timestamps.get(name); return 0L; } public final void setTimeStamp(final String name, final long value) { this.getTimestamps().put(name, value); } public ConcurrentHashMap getFriends() { return this.friends; } public final void claim(AbstractCharacter sourcePlayer) { // Clear any existing friend or condemn entries this.friends.clear(); DbManager.BuildingQueries.CLEAR_FRIENDS_LIST(this.getObjectUUID()); condemned.clear(); DbManager.BuildingQueries.CLEAR_CONDEMNED_LIST(this.getObjectUUID()); // Transfer the building asset ownership this.setOwner(sourcePlayer); } /** * @return the protectionState */ public ProtectionState getProtectionState() { return protectionState; } /** * @param protectionState the protectionState to set */ public void setProtectionState(ProtectionState protectionState) { // Early exit if protection state is already set to input value if (this.protectionState.equals(protectionState)) return; // if building is destroyed, just set the protection state. There isn't a DB // record to write anything to. if (rank == -1) { this.protectionState = protectionState; return; } if (DbManager.BuildingQueries.UPDATE_PROTECTIONSTATE(this.getObjectUUID(), protectionState) == true) { this.protectionState = protectionState; return; } Logger.error("Protection update failed for UUID: " + this.getObjectUUID() + "\n" + this.getBlueprint().getName() + " From " + this.protectionState.name() + " To: " + protectionState.name()); } public ConcurrentHashMap getCondemned() { return condemned; } public boolean setReverseKOS(boolean reverseKOS) { if (!DbManager.BuildingQueries.updateReverseKOS(this, reverseKOS)) return false; this.reverseKOS = reverseKOS; return true; } public boolean assetIsProtected() { boolean outValue = protectionState.equals(ProtectionState.PROTECTED); if (protectionState.equals(ProtectionState.CONTRACT)) outValue = true; return outValue; } public synchronized boolean transferGold(int amount, boolean tax) { if (amount < 0) if (!this.hasFunds(-amount)) return false; if (_strongboxValue + amount < 0) return false; if (_strongboxValue + amount > maxGold) return false; //Deduct Profit taxes. if (tax) if (taxType == TaxType.PROFIT && protectionState == ProtectionState.CONTRACT && amount > 0) amount = this.payProfitTaxes(amount); if (amount != 0) return this.setStrongboxValue(_strongboxValue + amount); return true; } public synchronized int payProfitTaxes(int amount) { if (this.getCity() == null) return amount; if (this.getCity().warehouse == null) return amount; if (this.getCity().warehouse.resources.get(ResourceType.GOLD) >= ResourceType.GOLD.deposit_limit) return amount; int profitAmount = (int) (amount * (taxAmount * .01f)); if (this.getCity().warehouse.resources.get(ResourceType.GOLD) + profitAmount <= ResourceType.GOLD.deposit_limit) { Warehouse.depositProfitTax(ResourceType.GOLD, profitAmount, this, this.getCity().warehouse); return amount - profitAmount; } //overDrafting int warehouseDeposit = ResourceType.GOLD.deposit_limit - this.getCity().warehouse.resources.get(ResourceType.GOLD); Warehouse.depositProfitTax(ResourceType.GOLD, warehouseDeposit, this, this.getCity().warehouse); return amount - warehouseDeposit; } public synchronized boolean setReserve(int amount, PlayerCharacter player) { if (!BuildingManager.playerCanManage(player, this)) return false; if (amount < 0) return false; if (!DbManager.BuildingQueries.SET_RESERVE(this, amount)) return false; this.reserve = amount; return true; } public synchronized boolean hasFunds(int amount) { return amount <= (this._strongboxValue - reserve); } public ArrayList getPatrolPoints() { return patrolPoints; } public ArrayList getSentryPoints() { return sentryPoints; } public synchronized boolean addProtectionTax(Building building, PlayerCharacter pc, final TaxType taxType, int amount, boolean enforceKOS) { if (building == null) return false; if (this.getBlueprint() == null) return false; if (this.getBlueprint().getBuildingGroup() != BuildingGroup.TOL) return false; if (building.assetIsProtected()) return false; if (!DbManager.BuildingQueries.addTaxes(building, taxType, amount, enforceKOS)) return false; building.taxType = taxType; building.taxAmount = amount; building.enforceKOS = enforceKOS; return true; } public synchronized boolean acceptTaxes() { if (!DbManager.BuildingQueries.acceptTaxes(this)) return false; this.setProtectionState(mbEnums.ProtectionState.CONTRACT); this.taxDateTime = LocalDateTime.now().plusDays(7); return true; } public synchronized boolean removeTaxes() { if (!DbManager.BuildingQueries.removeTaxes(this)) return false; this.taxType = TaxType.NONE; this.taxAmount = 0; this.taxDateTime = null; this.enforceKOS = false; return true; } public void AddToBarracksList() { City playerCity = ZoneManager.getCityAtLocation(this.loc); if (playerCity != null) { playerCity.cityBarracks.add(this); } } public void RemoveFromBarracksList() { } }