You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1365 lines
43 KiB
1365 lines
43 KiB
// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ . |
|
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌· |
|
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀ |
|
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌ |
|
// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀ |
|
// Magicbane Emulator Project © 2013 - 2022 |
|
// www.magicbane.com |
|
|
|
|
|
package engine.objects; |
|
|
|
import engine.Enum; |
|
import engine.Enum.*; |
|
import engine.InterestManagement.HeightMap; |
|
import engine.InterestManagement.RealmMap; |
|
import engine.InterestManagement.WorldGrid; |
|
import engine.db.archive.CityRecord; |
|
import engine.db.archive.DataWarehouse; |
|
import engine.gameManager.*; |
|
import engine.math.Bounds; |
|
import engine.math.FastMath; |
|
import engine.math.Vector2f; |
|
import engine.math.Vector3fImmutable; |
|
import engine.net.ByteBufferWriter; |
|
import engine.net.Dispatch; |
|
import engine.net.DispatchMessage; |
|
import engine.net.client.msg.ErrorPopupMsg; |
|
import engine.net.client.msg.TaxResourcesMsg; |
|
import engine.net.client.msg.ViewResourcesMessage; |
|
import engine.powers.EffectsBase; |
|
import engine.server.MBServerStatics; |
|
import engine.server.world.WorldServer; |
|
import engine.workthreads.DestroyCityThread; |
|
import engine.workthreads.TransferCityThread; |
|
import org.pmw.tinylog.Logger; |
|
|
|
import java.sql.ResultSet; |
|
import java.sql.SQLException; |
|
import java.time.LocalDateTime; |
|
import java.time.ZoneId; |
|
import java.util.ArrayList; |
|
import java.util.HashSet; |
|
import java.util.Iterator; |
|
import java.util.concurrent.ConcurrentHashMap; |
|
import java.util.concurrent.ThreadLocalRandom; |
|
import java.util.concurrent.locks.ReentrantReadWriteLock; |
|
|
|
public class City extends AbstractWorldObject { |
|
|
|
public static long lastCityUpdate = 0; |
|
public final HashSet<Integer> _playerMemory = new HashSet<>(); |
|
public java.time.LocalDateTime established; |
|
public boolean hasBeenTransfered = false; |
|
public LocalDateTime realmTaxDate; |
|
public ReentrantReadWriteLock transactionLock = new ReentrantReadWriteLock(); |
|
public volatile boolean protectionEnforced = true; |
|
public ArrayList<Building> cityBarracks; |
|
public ArrayList<Integer> cityOutlaws = new ArrayList<>(); |
|
protected Zone parentZone; |
|
private String cityName; |
|
private String motto; |
|
private String description; |
|
private int isNoobIsle; //1: noob, 0: not noob: -1: not noob, no teleport |
|
private int population = 0; |
|
private int siegesWithstood = 0; |
|
private int realmID; |
|
private int radiusType; |
|
private float bindRadius; |
|
private float bindX; |
|
private float bindZ; |
|
private byte isNpc; //aka Safehold |
|
private byte isCapital = 0; |
|
private byte isSafeHold; |
|
private boolean forceRename = false; |
|
private boolean noTeleport = false; //used by npc cities |
|
private boolean noRepledge = false; //used by npc cities |
|
private final boolean isOpen = false; |
|
private int treeOfLifeID; |
|
private Vector3fImmutable location = Vector3fImmutable.ZERO; |
|
|
|
// Players who have entered the city (used for adding and removing affects) |
|
private Vector3fImmutable bindLoc; |
|
private int warehouseBuildingID = 0; |
|
private boolean open = false; |
|
private boolean reverseKOS = false; |
|
private String hash; |
|
|
|
/** |
|
* ResultSet Constructor |
|
*/ |
|
|
|
public City(ResultSet rs) throws SQLException { |
|
super(rs); |
|
try { |
|
this.cityName = rs.getString("name"); |
|
this.motto = rs.getString("motto"); |
|
this.isNpc = rs.getByte("isNpc"); |
|
this.isSafeHold = (byte) ((this.isNpc == 1) ? 1 : 0); |
|
this.description = ""; // TODO Implement this! |
|
this.isNoobIsle = rs.getByte("isNoobIsle"); // Noob |
|
this.gridObjectType = GridObjectType.STATIC; |
|
// Island |
|
// City(00000001), |
|
// Otherwise(FFFFFFFF) |
|
this.population = rs.getInt("population"); |
|
this.siegesWithstood = rs.getInt("siegesWithstood"); |
|
|
|
java.sql.Timestamp establishedTimeStamp = rs.getTimestamp("established"); |
|
|
|
if (establishedTimeStamp != null) |
|
this.established = java.time.LocalDateTime.ofInstant(establishedTimeStamp.toInstant(), ZoneId.systemDefault()); |
|
|
|
this.location = Vector3fImmutable.ZERO; |
|
|
|
java.sql.Timestamp realmTaxTimeStamp = rs.getTimestamp("realmTaxDate"); |
|
|
|
if (realmTaxTimeStamp != null) |
|
this.realmTaxDate = realmTaxTimeStamp.toLocalDateTime(); |
|
|
|
if (this.realmTaxDate == null) |
|
this.realmTaxDate = LocalDateTime.now(); |
|
|
|
this.treeOfLifeID = rs.getInt("treeOfLifeUUID"); |
|
this.bindX = rs.getFloat("bindX"); |
|
this.bindZ = rs.getFloat("bindZ"); |
|
this.bindLoc = new Vector3fImmutable(this.location.getX() + this.bindX, |
|
this.location.getY(), |
|
this.location.getZ() + this.bindZ); |
|
this.radiusType = rs.getInt("radiusType"); |
|
float bindradiustemp = rs.getFloat("bindRadius"); |
|
if (bindradiustemp > 2) |
|
bindradiustemp -= 2; |
|
|
|
this.bindRadius = bindradiustemp; |
|
|
|
this.forceRename = rs.getInt("forceRename") == 1; |
|
this.open = rs.getInt("open") == 1; |
|
|
|
if (this.cityName.equals("Perdition") || this.cityName.equals("Bastion")) { |
|
this.noTeleport = true; |
|
this.noRepledge = true; |
|
} else { |
|
this.noTeleport = false; |
|
this.noRepledge = false; |
|
} |
|
|
|
this.hash = rs.getString("hash"); |
|
|
|
if (this.motto.isEmpty()) { |
|
Guild guild = this.getGuild(); |
|
|
|
if (guild != null && guild.isEmptyGuild() == false) |
|
this.motto = guild.getMotto(); |
|
} |
|
|
|
Zone zone = ZoneManager.getZoneByUUID(rs.getInt("parent")); |
|
|
|
if (zone != null) |
|
setParent(zone); |
|
|
|
//npc cities without heightmaps except swampstone are specials. |
|
|
|
this.realmID = rs.getInt("realmID"); |
|
|
|
} catch (Exception e) { |
|
Logger.error(e); |
|
} |
|
|
|
} |
|
|
|
/* |
|
* Utils |
|
*/ |
|
|
|
public static void _serializeForClientMsg(City city, ByteBufferWriter writer) { |
|
City.serializeForClientMsg(city, writer); |
|
} |
|
|
|
public static void serializeForClientMsg(City city, ByteBufferWriter writer) { |
|
AbstractCharacter guildRuler; |
|
Guild rulingGuild; |
|
Guild rulingNation; |
|
java.time.LocalDateTime dateTime1900; |
|
|
|
// Cities aren't a city without a TOL. Time to early exit. |
|
// No need to spam the log here as non-existant TOL's are indicated |
|
// during bootstrap routines. |
|
|
|
if (city.getTOL() == null) { |
|
|
|
Logger.error("NULL TOL FOR " + city.cityName); |
|
} |
|
|
|
// Assign city owner |
|
|
|
if (city.getTOL() != null) |
|
guildRuler = city.getTOL().getOwner(); |
|
else |
|
guildRuler = null; |
|
|
|
// If is an errant tree, use errant guild for serialization. |
|
// otherwise we serialize the soverign guild |
|
|
|
if (guildRuler == null) |
|
rulingGuild = Guild.getErrantGuild(); |
|
else |
|
rulingGuild = guildRuler.getGuild(); |
|
|
|
rulingNation = rulingGuild.getNation(); |
|
|
|
// Begin Serializing sovereign guild data |
|
|
|
writer.putInt(city.getObjectType().ordinal()); |
|
writer.putInt(city.getObjectUUID()); |
|
writer.putString(city.cityName); |
|
writer.putInt(rulingGuild.getObjectType().ordinal()); |
|
writer.putInt(rulingGuild.getObjectUUID()); |
|
|
|
writer.putString(rulingGuild.getName()); |
|
writer.putString(city.motto); |
|
writer.putString(rulingGuild.getLeadershipType()); |
|
|
|
// Serialize guild ruler's name |
|
// If tree is abandoned blank out the name |
|
// to allow them a rename. |
|
|
|
if (guildRuler == null) |
|
writer.putString(""); |
|
else |
|
writer.putString(guildRuler.getFirstName() + ' ' + guildRuler.getLastName()); |
|
|
|
writer.putInt(rulingGuild.getCharter()); |
|
writer.putInt(0); // always 00000000 |
|
|
|
writer.put(city.isSafeHold); |
|
|
|
writer.put((byte) 1); |
|
writer.put((byte) 1); // *** Refactor: What are these flags? |
|
writer.put((byte) 1); |
|
writer.put((byte) 1); |
|
writer.put((byte) 1); |
|
|
|
GuildTag._serializeForDisplay(rulingGuild.getGuildTag(), writer); |
|
GuildTag._serializeForDisplay(rulingNation.getGuildTag(), writer); |
|
|
|
writer.putInt(0);// TODO Implement description text |
|
|
|
writer.put((byte) 1); |
|
|
|
if (city.isCapital > 0) |
|
writer.put((byte) 1); |
|
else |
|
writer.put((byte) 0); |
|
|
|
writer.put((byte) 1); |
|
|
|
// Begin serializing nation guild info |
|
|
|
if (rulingNation.isEmptyGuild()) { |
|
writer.putInt(rulingGuild.getObjectType().ordinal()); |
|
writer.putInt(rulingGuild.getObjectUUID()); |
|
} else { |
|
writer.putInt(rulingNation.getObjectType().ordinal()); |
|
writer.putInt(rulingNation.getObjectUUID()); |
|
} |
|
|
|
// Serialize nation name |
|
|
|
if (rulingNation.isEmptyGuild()) |
|
writer.putString("None"); |
|
else |
|
writer.putString(rulingNation.getName()); |
|
|
|
writer.putInt(city.getTOL().getRank()); |
|
|
|
if (city.isNoobIsle > 0) |
|
writer.putInt(1); |
|
else |
|
writer.putInt(0xFFFFFFFF); |
|
|
|
writer.putInt(city.population); |
|
|
|
if (rulingNation.isEmptyGuild()) |
|
writer.putString(" "); |
|
else |
|
writer.putString(Guild.GetGL(rulingNation).getFirstName() + ' ' + Guild.GetGL(rulingNation).getLastName()); |
|
|
|
writer.putLocalDateTime(city.established); |
|
|
|
writer.putFloat(city.location.x); |
|
writer.putFloat(city.location.y); |
|
writer.putFloat(city.location.z); |
|
|
|
writer.putInt(city.siegesWithstood); |
|
|
|
writer.put((byte) 1); |
|
writer.put((byte) 0); |
|
writer.putInt(0x64); |
|
writer.put((byte) 0); |
|
writer.put((byte) 0); |
|
writer.put((byte) 0); |
|
} |
|
|
|
public static Vector3fImmutable getBindLoc(int cityID) { |
|
|
|
City city; |
|
|
|
city = City.getCity(cityID); |
|
|
|
if (city == null) |
|
return Enum.Ruins.getRandomRuin().getLocation(); |
|
|
|
return city.getBindLoc(); |
|
} |
|
|
|
public static ArrayList<City> getCitiesToTeleportTo(PlayerCharacter pc) { |
|
|
|
ArrayList<City> cities = new ArrayList<>(); |
|
|
|
if (pc == null) |
|
return cities; |
|
|
|
Guild pcG = pc.getGuild(); |
|
|
|
ConcurrentHashMap<Integer, AbstractGameObject> worldCities = DbManager.getMap(Enum.GameObjectType.City); |
|
|
|
//add npc cities |
|
|
|
for (AbstractGameObject ago : worldCities.values()) { |
|
|
|
if (ago.getObjectType().equals(GameObjectType.City)) { |
|
City city = (City) ago; |
|
|
|
if (city.noTeleport) |
|
continue; |
|
|
|
if (city.parentZone != null && city.parentZone.isPlayerCity()) { |
|
|
|
if (pc.getAccount().status.equals(AccountStatus.ADMIN)) { |
|
cities.add(city); |
|
} else |
|
//list Player cities |
|
|
|
//open city, just list |
|
|
|
if (city.open && city.getTOL() != null && city.getTOL().getRank() > 4) { |
|
|
|
if (!BuildingManager.IsPlayerHostile(city.getTOL(), pc)) |
|
cities.add(city); //verify nation or guild is same |
|
} else if (Guild.sameNationExcludeErrant(city.getGuild(), pcG)) |
|
cities.add(city); |
|
|
|
|
|
} else if (city.isNpc == 1) { |
|
|
|
//list NPC cities |
|
|
|
Guild g = city.getGuild(); |
|
if (g == null) { |
|
if (city.isNpc == 1) |
|
if (city.isNoobIsle == 1) { |
|
if (pc.getLevel() < 21) |
|
cities.add(city); |
|
} else if (pc.getLevel() > 9) |
|
cities.add(city); |
|
|
|
} else if (pc.getLevel() >= g.getTeleportMin() && pc.getLevel() <= g.getTeleportMax()) |
|
cities.add(city); |
|
} |
|
|
|
} |
|
} |
|
|
|
return cities; |
|
} |
|
|
|
public static ArrayList<City> getCitiesToRepledgeTo(PlayerCharacter playerCharacter) { |
|
|
|
ArrayList<City> cities = new ArrayList<>(); |
|
|
|
if (playerCharacter == null) |
|
return cities; |
|
|
|
Guild pcG = playerCharacter.getGuild(); |
|
|
|
ConcurrentHashMap<Integer, AbstractGameObject> worldCities = DbManager.getMap(Enum.GameObjectType.City); |
|
|
|
//add npc cities |
|
|
|
for (AbstractGameObject ago : worldCities.values()) { |
|
if (ago.getObjectType().equals(GameObjectType.City)) { |
|
|
|
City city = (City) ago; |
|
|
|
if (city.noRepledge) |
|
continue; |
|
|
|
if (city.parentZone != null && city.parentZone.isPlayerCity()) { |
|
|
|
//list Player cities |
|
//open city, just list |
|
|
|
if (playerCharacter.getAccount().status.equals(AccountStatus.ADMIN)) { |
|
cities.add(city); |
|
} else if (city.open && city.getTOL() != null && city.getTOL().getRank() > 4) { |
|
|
|
if (!BuildingManager.IsPlayerHostile(city.getTOL(), playerCharacter)) |
|
cities.add(city); //verify nation or guild is same |
|
} else if (Guild.sameNationExcludeErrant(city.getGuild(), pcG)) |
|
cities.add(city); |
|
|
|
} else if (city.isNpc == 1) { |
|
//list NPC cities |
|
|
|
Guild guild = city.getGuild(); |
|
|
|
if (guild == null) { |
|
if (city.isNpc == 1) |
|
if (city.isNoobIsle == 1) { |
|
if (playerCharacter.getLevel() < 21) |
|
cities.add(city); |
|
} else if (playerCharacter.getLevel() > 9) |
|
cities.add(city); |
|
} else if (playerCharacter.getLevel() >= guild.getRepledgeMin() && playerCharacter.getLevel() <= guild.getRepledgeMax()) { |
|
|
|
cities.add(city); |
|
} |
|
} |
|
} |
|
} |
|
return cities; |
|
} |
|
|
|
public static City getCity(int cityId) { |
|
|
|
if (cityId == 0) |
|
return null; |
|
|
|
City city = (City) DbManager.getFromCache(Enum.GameObjectType.City, cityId); |
|
|
|
if (city != null) |
|
return city; |
|
|
|
return DbManager.CityQueries.GET_CITY(cityId); |
|
|
|
} |
|
|
|
public static City GetCityFromCache(int cityId) { |
|
|
|
if (cityId == 0) |
|
return null; |
|
|
|
return (City) DbManager.getFromCache(Enum.GameObjectType.City, cityId); |
|
} |
|
|
|
public boolean renameCity(String cityName) { |
|
|
|
if (!DbManager.CityQueries.renameCity(this, cityName)) |
|
return false; |
|
|
|
if (!DbManager.CityQueries.updateforceRename(this, false)) |
|
return false; |
|
|
|
this.cityName = cityName; |
|
this.forceRename = false; |
|
return true; |
|
} |
|
|
|
public boolean updateTOL(Building tol) { |
|
|
|
if (tol == null) |
|
return false; |
|
|
|
if (!DbManager.CityQueries.updateTOL(this, tol.getObjectUUID())) |
|
return false; |
|
|
|
this.treeOfLifeID = tol.getObjectUUID(); |
|
return true; |
|
} |
|
|
|
public boolean renameCityForNewPlant(String cityName) { |
|
|
|
if (!DbManager.CityQueries.renameCity(this, cityName)) |
|
return false; |
|
|
|
if (!DbManager.CityQueries.updateforceRename(this, true)) |
|
return false; |
|
|
|
this.cityName = cityName; |
|
this.forceRename = true; |
|
return true; |
|
} |
|
|
|
public String getCityName() { |
|
|
|
return cityName; |
|
} |
|
|
|
public String getMotto() { |
|
return motto; |
|
} |
|
|
|
public String getDescription() { |
|
return description; |
|
} |
|
|
|
public Building getTOL() { |
|
|
|
if (this.treeOfLifeID == 0) |
|
return null; |
|
|
|
return BuildingManager.getBuildingFromCache(this.treeOfLifeID); |
|
} |
|
|
|
/** |
|
* @param population the population to set |
|
*/ |
|
public void setPopulation(int population) { |
|
this.population = population; |
|
} |
|
|
|
public int getSiegesWithstood() { |
|
return siegesWithstood; |
|
} |
|
|
|
/** |
|
* @param siegesWithstood the siegesWithstood to set |
|
*/ |
|
public void setSiegesWithstood(int siegesWithstood) { |
|
|
|
// early exit if setting to current value |
|
|
|
if (this.siegesWithstood == siegesWithstood) |
|
return; |
|
|
|
if (DbManager.CityQueries.updateSiegesWithstood(this, siegesWithstood) == true) |
|
this.siegesWithstood = siegesWithstood; |
|
else |
|
Logger.error("Error when writing to database for cityUUID: " + this.getObjectUUID()); |
|
} |
|
|
|
public float getAltitude() { |
|
return this.location.y; |
|
} |
|
|
|
@Override |
|
public Vector3fImmutable getLoc() { |
|
return this.location; |
|
} |
|
|
|
public boolean isSafeHold() { |
|
return (this.isSafeHold == (byte) 1); |
|
} |
|
|
|
public int getRank() { |
|
return (this.getTOL() == null) ? 0 : this.getTOL().getRank(); |
|
} |
|
|
|
/* |
|
* Serializing |
|
*/ |
|
|
|
public Bane getBane() { |
|
return Bane.getBane(this.getObjectUUID()); |
|
} |
|
|
|
public Zone getParent() { |
|
return this.parentZone; |
|
} |
|
|
|
public void setParent(Zone zone) { |
|
|
|
try { |
|
|
|
|
|
this.parentZone = zone; |
|
this.location = new Vector3fImmutable(zone.absX, zone.absY, zone.absZ); |
|
this.bindLoc = new Vector3fImmutable(this.location.x + this.bindX, |
|
this.location.y, |
|
this.location.z + this.bindZ); |
|
|
|
// set city bounds |
|
|
|
Bounds cityBounds = Bounds.borrow(); |
|
cityBounds.setBounds(new Vector2f(this.location.x + 64, this.location.z + 64), // location x and z are offset by 64 from the center of the city. |
|
new Vector2f(Enum.CityBoundsType.GRID.extents, Enum.CityBoundsType.GRID.extents), |
|
0.0f); |
|
this.setBounds(cityBounds); |
|
|
|
if (zone.getHeightMap() == null && this.isNpc == 1 && this.getObjectUUID() != 1213) { |
|
HeightMap.GenerateCustomHeightMap(zone); |
|
Logger.info(zone.getName() + " created custom heightmap"); |
|
} |
|
} catch (Exception e) { |
|
Logger.error(e); |
|
} |
|
} |
|
|
|
public AbstractCharacter getOwner() { |
|
|
|
if (this.getTOL() == null) |
|
return null; |
|
|
|
int ownerID = this.getTOL().getOwnerUUID(); |
|
|
|
if (ownerID == 0) |
|
return null; |
|
|
|
if (this.isNpc == 1) |
|
return NPC.getNPC(ownerID); |
|
else |
|
return PlayerCharacter.getPlayerCharacter(ownerID); |
|
} |
|
|
|
public Guild getGuild() { |
|
|
|
if (this.getTOL() == null) |
|
return null; |
|
|
|
if (this.isNpc == 1) { |
|
|
|
if (this.getTOL().getOwner() == null) |
|
return null; |
|
return this.getTOL().getOwner().getGuild(); |
|
} else { |
|
|
|
if (this.getTOL().getOwner() == null) |
|
return null; |
|
return this.getTOL().getOwner().getGuild(); |
|
} |
|
} |
|
|
|
public boolean openCity(boolean open) { |
|
|
|
if (!DbManager.CityQueries.updateOpenCity(this, open)) |
|
return false; |
|
|
|
this.open = open; |
|
return true; |
|
} |
|
|
|
public Vector3fImmutable getBindLoc() { |
|
|
|
Vector3fImmutable treeLoc = null; |
|
|
|
if (this.getTOL() != null && this.getTOL().getRank() == 8) |
|
treeLoc = this.getTOL().getStuckLocation(); |
|
|
|
if (treeLoc != null) |
|
return treeLoc; |
|
|
|
if (this.radiusType == 1 && this.bindRadius > 0f) { |
|
|
|
//square radius |
|
|
|
float x = this.bindLoc.getX(); |
|
float z = this.bindLoc.getZ(); |
|
float offset = ((ThreadLocalRandom.current().nextFloat() * 2) - 1) * this.bindRadius; |
|
int direction = ThreadLocalRandom.current().nextInt(4); |
|
|
|
switch (direction) { |
|
case 0: |
|
x += this.bindRadius; |
|
z += offset; |
|
break; |
|
case 1: |
|
x += offset; |
|
z -= this.bindRadius; |
|
break; |
|
case 2: |
|
x -= this.bindRadius; |
|
z += offset; |
|
break; |
|
case 3: |
|
x += offset; |
|
z += this.bindRadius; |
|
break; |
|
} |
|
return new Vector3fImmutable(x, this.bindLoc.getY(), z); |
|
} else if (this.radiusType == 2 && this.bindRadius > 0f) { |
|
//circle radius |
|
Vector3fImmutable dir = FastMath.randomVector2D(); |
|
return this.bindLoc.scaleAdd(this.bindRadius, dir); |
|
} else if (this.radiusType == 3 && this.bindRadius > 0f) { |
|
//random inside square |
|
float x = this.bindLoc.getX(); |
|
x += ((ThreadLocalRandom.current().nextFloat() * 2) - 1) * this.bindRadius; |
|
float z = this.bindLoc.getZ(); |
|
z += ((ThreadLocalRandom.current().nextFloat() * 2) - 1) * this.bindRadius; |
|
return new Vector3fImmutable(x, this.bindLoc.getY(), z); |
|
} else if (this.radiusType == 4 && this.bindRadius > 0f) { |
|
//random inside circle |
|
Vector3fImmutable dir = FastMath.randomVector2D(); |
|
return this.bindLoc.scaleAdd(ThreadLocalRandom.current().nextFloat() * this.bindRadius, dir); |
|
} else |
|
//spawn at bindLoc |
|
//System.out.println("x: " + this.bindLoc.x + ", z: " + this.bindLoc.z); |
|
return this.bindLoc; |
|
} |
|
|
|
public NPC getRuneMaster() { |
|
NPC outNPC = null; |
|
|
|
if (this.getTOL() == null) |
|
return outNPC; |
|
|
|
for (AbstractCharacter npc : getTOL().getHirelings().keySet()) { |
|
if (npc.getObjectType() == GameObjectType.NPC) |
|
if (((NPC) npc).getContract().isRuneMaster() == true) |
|
outNPC = (NPC) npc; |
|
} |
|
|
|
return outNPC; |
|
} |
|
|
|
public boolean isOpen() { |
|
return open; |
|
} |
|
|
|
@Override |
|
public void updateDatabase() { |
|
// TODO Create update logic. |
|
} |
|
|
|
@Override |
|
public void runAfterLoad() { |
|
|
|
// Set city bounds |
|
// *** Note: Moved to SetParent() |
|
// for some undocumented reason |
|
|
|
// Set city motto to current guild motto |
|
|
|
if (BuildingManager.getBuilding(this.treeOfLifeID) == null) |
|
Logger.info("City UID " + this.getObjectUUID() + " Failed to Load Tree of Life with ID " + this.treeOfLifeID); |
|
|
|
if ((ConfigManager.serverType.equals(ServerType.WORLDSERVER)) |
|
&& (this.isNpc == (byte) 0)) { |
|
|
|
Realm wsr = Realm.getRealm(this.realmID); |
|
|
|
if (wsr != null) |
|
wsr.addCity(this.getObjectUUID()); |
|
else |
|
Logger.error("Unable to find realm of ID " + realmID + " for city " + this.getObjectUUID()); |
|
} |
|
|
|
if (this.getGuild() != null) { |
|
this.motto = this.getGuild().getMotto(); |
|
|
|
// Determine if this city is a nation capitol |
|
|
|
if (this.getGuild().getGuildState() == GuildState.Nation) |
|
for (Guild sub : this.getGuild().getSubGuildList()) { |
|
|
|
if ((sub.getGuildState() == GuildState.Protectorate) || |
|
(sub.getGuildState() == GuildState.Province)) |
|
this.isCapital = 1; |
|
} |
|
|
|
ArrayList<PlayerCharacter> guildList = Guild.GuildRoster(this.getGuild()); |
|
|
|
this.population = guildList.size(); |
|
} |
|
|
|
// Banes are loaded for this city from the database at this point |
|
|
|
if (this.getBane() == null) |
|
return; |
|
|
|
// if this city is baned, add the siege effect |
|
|
|
try { |
|
this.getTOL().addEffectBit((1 << 16)); |
|
this.getBane().getStone().addEffectBit((1 << 19)); |
|
} catch (Exception e) { |
|
Logger.info("Failed ao add bane effects on city." + e.getMessage()); |
|
} |
|
} |
|
|
|
public void addCityEffect(EffectsBase effectBase, int rank) { |
|
|
|
HashSet<AbstractWorldObject> currentPlayers; |
|
PlayerCharacter player; |
|
|
|
// Add this new effect to the current city effect collection. |
|
// so any new player to the grid will have all effects applied |
|
|
|
this.addEffectNoTimer(Integer.toString(effectBase.getUUID()), effectBase, rank, false); |
|
|
|
// Any players currently in the zone will not be processed by the heartbeat |
|
// if it's not the first effect toggled so we do it here manually |
|
|
|
currentPlayers = WorldGrid.getObjectsInRangePartial(this.location, this.parentZone.getBounds().getHalfExtents().x * 1.2f, MBServerStatics.MASK_PLAYER); |
|
|
|
for (AbstractWorldObject playerObject : currentPlayers) { |
|
|
|
if (playerObject == null) |
|
continue; |
|
|
|
if (!this.isLocationWithinSiegeBounds(playerObject.getLoc())) |
|
continue; |
|
|
|
player = (PlayerCharacter) playerObject; |
|
player.addCityEffect(Integer.toString(effectBase.getUUID()), effectBase, rank, MBServerStatics.FOURTYFIVE_SECONDS, true, this); |
|
} |
|
|
|
} |
|
|
|
public void removeCityEffect(EffectsBase effectBase, int rank, boolean refreshEffect) { |
|
|
|
|
|
PlayerCharacter player; |
|
|
|
// Remove the city effect from the ago's internal collection |
|
|
|
this.getEffects().remove(Integer.toString(effectBase.getUUID())); |
|
|
|
// Any players currently in the zone will not be processed by the heartbeat |
|
// so we do it here manually |
|
|
|
|
|
for (Integer playerID : this._playerMemory) { |
|
|
|
player = PlayerCharacter.getFromCache(playerID); |
|
|
|
if (player == null) |
|
continue; |
|
|
|
player.endEffectNoPower(Integer.toString(effectBase.getUUID())); |
|
|
|
// Reapply effect with timeout? |
|
|
|
if (refreshEffect == true) |
|
player.addCityEffect(Integer.toString(effectBase.getUUID()), effectBase, rank, MBServerStatics.FOURTYFIVE_SECONDS, false, this); |
|
|
|
} |
|
|
|
} |
|
|
|
public Warehouse getWarehouse() { |
|
|
|
if (this.warehouseBuildingID == 0) |
|
return null; |
|
|
|
return Warehouse.warehouseByBuildingUUID.get(this.warehouseBuildingID); |
|
} |
|
|
|
public Realm getRealm() { |
|
|
|
return Realm.getRealm(this.realmID); |
|
|
|
} |
|
|
|
public boolean isLocationOnCityGrid(Vector3fImmutable insideLoc) { |
|
|
|
Bounds newBounds = Bounds.borrow(); |
|
newBounds.setBounds(insideLoc); |
|
boolean collided = Bounds.collide(this.getBounds(), newBounds, 0); |
|
newBounds.release(); |
|
return collided; |
|
} |
|
|
|
public boolean isLocationOnCityGrid(Bounds newBounds) { |
|
|
|
boolean collided = Bounds.collide(this.getBounds(), newBounds, 0); |
|
return collided; |
|
} |
|
|
|
public boolean isLocationWithinSiegeBounds(Vector3fImmutable insideLoc) { |
|
|
|
return insideLoc.isInsideCircle(this.getLoc(), CityBoundsType.ZONE.extents); |
|
|
|
} |
|
|
|
public boolean isLocationOnCityZone(Vector3fImmutable insideLoc) { |
|
return Bounds.collide(insideLoc, this.parentZone.getBounds()); |
|
} |
|
|
|
private void applyAllCityEffects(PlayerCharacter player) { |
|
|
|
Effect effect; |
|
EffectsBase effectBase; |
|
|
|
try { |
|
for (String cityEffect : this.getEffects().keySet()) { |
|
|
|
effect = this.getEffects().get(cityEffect); |
|
effectBase = effect.getEffectsBase(); |
|
|
|
if (effectBase == null) |
|
continue; |
|
|
|
player.addCityEffect(Integer.toString(effectBase.getUUID()), effectBase, effect.getTrains(), MBServerStatics.FOURTYFIVE_SECONDS, true, this); |
|
} |
|
} catch (Exception e) { |
|
Logger.error(e.getMessage()); |
|
} |
|
|
|
} |
|
|
|
private void removeAllCityEffects(PlayerCharacter player, boolean force) { |
|
|
|
Effect effect; |
|
EffectsBase effectBase; |
|
|
|
try { |
|
for (String cityEffect : this.getEffects().keySet()) { |
|
|
|
effect = this.getEffects().get(cityEffect); |
|
effectBase = effect.getEffectsBase(); |
|
|
|
if (player.getEffects().get(cityEffect) == null) |
|
return; |
|
|
|
// player.endEffectNoPower(cityEffect); |
|
player.addCityEffect(Integer.toString(effectBase.getUUID()), effectBase, effect.getTrains(), MBServerStatics.FOURTYFIVE_SECONDS, false, this); |
|
} |
|
} catch (Exception e) { |
|
Logger.error(e.getMessage()); |
|
} |
|
} |
|
|
|
/* All characters in city player memory but |
|
* not the current memory have obviously |
|
* left the city. Remove their affects. |
|
*/ |
|
|
|
public void onEnter() { |
|
|
|
HashSet<AbstractWorldObject> currentPlayers; |
|
HashSet<Integer> currentMemory; |
|
PlayerCharacter player; |
|
|
|
// Gather current list of players within the zone bounds |
|
|
|
currentPlayers = WorldGrid.getObjectsInRangePartial(this.location, CityBoundsType.ZONE.extents, MBServerStatics.MASK_PLAYER); |
|
currentMemory = new HashSet<>(); |
|
|
|
for (AbstractWorldObject playerObject : currentPlayers) { |
|
|
|
if (playerObject == null) |
|
continue; |
|
|
|
player = (PlayerCharacter) playerObject; |
|
currentMemory.add(player.getObjectUUID()); |
|
|
|
// Player is already in our memory |
|
|
|
if (_playerMemory.contains(player.getObjectUUID())) |
|
continue; |
|
|
|
if (!this.isLocationWithinSiegeBounds(player.getLoc())) |
|
continue; |
|
// Apply safehold affect to player if needed |
|
|
|
if ((this.isSafeHold == 1)) |
|
player.setSafeZone(true); |
|
|
|
//add spire effects. |
|
if (this.getEffects().size() > 0) |
|
this.applyAllCityEffects(player); |
|
|
|
// Add player to our city's memory |
|
|
|
_playerMemory.add(player.getObjectUUID()); |
|
|
|
// ***For debugging |
|
// Logger.info("PlayerMemory for ", this.getCityName() + ": " + _playerMemory.size()); |
|
} |
|
try { |
|
onExit(currentMemory); |
|
} catch (Exception e) { |
|
Logger.error(e.getMessage()); |
|
} |
|
|
|
} |
|
|
|
private void onExit(HashSet<Integer> currentMemory) { |
|
|
|
PlayerCharacter player; |
|
int playerUUID = 0; |
|
HashSet<Integer> toRemove = new HashSet<>(); |
|
Iterator<Integer> iter = _playerMemory.iterator(); |
|
|
|
while (iter.hasNext()) { |
|
|
|
playerUUID = iter.next(); |
|
|
|
|
|
player = PlayerCharacter.getFromCache(playerUUID); |
|
|
|
if (this.isLocationWithinSiegeBounds(player.getLoc())) |
|
continue; |
|
|
|
// Remove players safezone status if warranted |
|
// they can assumed to be not on the citygrid at |
|
// this point. |
|
|
|
|
|
player.setSafeZone(false); |
|
|
|
this.removeAllCityEffects(player, false); |
|
|
|
// We will remove this player after iteration is complete |
|
// so store it in a temporary collection |
|
|
|
toRemove.add(playerUUID); |
|
// ***For debugging |
|
// Logger.info("PlayerMemory for ", this.getCityName() + ": " + _playerMemory.size()); |
|
} |
|
|
|
// Remove players from city memory |
|
|
|
_playerMemory.removeAll(toRemove); |
|
for (Integer removalUUID : toRemove) { |
|
this.cityOutlaws.remove(removalUUID); |
|
} |
|
} |
|
|
|
public int getWarehouseBuildingID() { |
|
return warehouseBuildingID; |
|
} |
|
|
|
public void setWarehouseBuildingID(int warehouseBuildingID) { |
|
this.warehouseBuildingID = warehouseBuildingID; |
|
} |
|
|
|
public final void destroy() { |
|
|
|
Thread destroyCityThread = new Thread(new DestroyCityThread(this)); |
|
|
|
destroyCityThread.setName("destroyCity:" + this.getName()); |
|
destroyCityThread.start(); |
|
} |
|
|
|
public final void transfer(AbstractCharacter newOwner) { |
|
|
|
Thread transferCityThread = new Thread(new TransferCityThread(this, newOwner)); |
|
|
|
transferCityThread.setName("TransferCity:" + this.getName()); |
|
transferCityThread.start(); |
|
} |
|
|
|
public final void claim(AbstractCharacter sourcePlayer) { |
|
|
|
Guild sourceNation; |
|
Guild sourceGuild; |
|
Zone cityZone; |
|
|
|
sourceGuild = sourcePlayer.getGuild(); |
|
|
|
if (sourceGuild == null) |
|
return; |
|
|
|
sourceNation = sourcePlayer.getGuild().getNation(); |
|
|
|
if (sourceGuild.isEmptyGuild()) |
|
return; |
|
|
|
//cant claim tree with owned tree. |
|
|
|
if (sourceGuild.getOwnedCity() != null) |
|
return; |
|
|
|
cityZone = this.parentZone; |
|
|
|
// Can't claim a tree not in a player city zone |
|
|
|
// Reset sieges withstood |
|
|
|
this.setSiegesWithstood(0); |
|
|
|
this.hasBeenTransfered = true; |
|
|
|
// If currently a sub of another guild, desub when |
|
// claiming your new tree and set as Landed |
|
|
|
if (!sourceNation.isEmptyGuild() && sourceNation != sourceGuild) { |
|
if (!DbManager.GuildQueries.UPDATE_PARENT(sourceGuild.getObjectUUID(), WorldServer.worldUUID)) { |
|
ChatManager.chatGuildError((PlayerCharacter) sourcePlayer, "A Serious error has occurred. Please post details for to ensure transaction integrity"); |
|
return; |
|
} |
|
|
|
sourceNation.getSubGuildList().remove(sourceGuild); |
|
|
|
if (sourceNation.getSubGuildList().isEmpty()) |
|
sourceNation.downgradeGuildState(); |
|
} |
|
|
|
// Link the mew guild with the tree |
|
|
|
if (!DbManager.GuildQueries.SET_GUILD_OWNED_CITY(sourceGuild.getObjectUUID(), this.getObjectUUID())) { |
|
ChatManager.chatGuildError((PlayerCharacter) sourcePlayer, "A Serious error has occurred. Please post details for to ensure transaction integrity"); |
|
return; |
|
} |
|
|
|
sourceGuild.setCityUUID(this.getObjectUUID()); |
|
|
|
sourceGuild.setNation(sourceGuild); |
|
sourceGuild.setGuildState(GuildState.Sovereign); |
|
GuildManager.updateAllGuildTags(sourceGuild); |
|
GuildManager.updateAllGuildBinds(sourceGuild, this); |
|
|
|
// Build list of buildings within this parent zone |
|
|
|
for (Building cityBuilding : cityZone.zoneBuildingSet) { |
|
|
|
// Buildings without blueprints are unclaimable |
|
|
|
if (cityBuilding.getBlueprintUUID() == 0) |
|
continue; |
|
|
|
// All protection contracts are void upon transfer of a city |
|
|
|
// All protection contracts are void upon transfer of a city |
|
//Dont forget to not Flip protection on Banestones and siege Equipment... Noob. |
|
|
|
if (cityBuilding.getBlueprint() != null && !cityBuilding.getBlueprint().isSiegeEquip() |
|
&& cityBuilding.getBlueprint().getBuildingGroup() != BuildingGroup.BANESTONE) |
|
cityBuilding.setProtectionState(ProtectionState.NONE); |
|
|
|
// Transfer ownership of valid city assets |
|
// these assets are autoprotected. |
|
|
|
if ((cityBuilding.getBlueprint().getBuildingGroup() == BuildingGroup.TOL) |
|
|| (cityBuilding.getBlueprint().getBuildingGroup() == BuildingGroup.SPIRE) |
|
|| (cityBuilding.getBlueprint().getBuildingGroup() == BuildingGroup.BARRACK) |
|
|| (cityBuilding.getBlueprint().isWallPiece()) |
|
|| (cityBuilding.getBlueprint().getBuildingGroup() == BuildingGroup.SHRINE)) { |
|
|
|
cityBuilding.claim(sourcePlayer); |
|
cityBuilding.setProtectionState(ProtectionState.PROTECTED); |
|
} |
|
} |
|
|
|
this.setForceRename(true); |
|
|
|
// Reset city timer for map update |
|
|
|
City.lastCityUpdate = System.currentTimeMillis(); |
|
} |
|
|
|
public final boolean transferGuildLeader(PlayerCharacter sourcePlayer) { |
|
|
|
Guild sourceGuild; |
|
Zone cityZone; |
|
sourceGuild = sourcePlayer.getGuild(); |
|
|
|
if (sourceGuild == null) |
|
return false; |
|
|
|
if (sourceGuild.isEmptyGuild()) |
|
return false; |
|
|
|
cityZone = this.parentZone; |
|
|
|
for (Building cityBuilding : cityZone.zoneBuildingSet) { |
|
|
|
// Buildings without blueprints are unclaimable |
|
|
|
if (cityBuilding.getBlueprintUUID() == 0) |
|
continue; |
|
|
|
// All protection contracts are void upon transfer of a city |
|
//Dont forget to not Flip protection on Banestones and siege Equipment... Noob. |
|
|
|
// Transfer ownership of valid city assets |
|
// these assets are autoprotected. |
|
|
|
if ((cityBuilding.getBlueprint().getBuildingGroup() == BuildingGroup.TOL) |
|
|| (cityBuilding.getBlueprint().getBuildingGroup() == BuildingGroup.SPIRE) |
|
|| (cityBuilding.getBlueprint().getBuildingGroup() == BuildingGroup.BARRACK) |
|
|| (cityBuilding.getBlueprint().isWallPiece()) |
|
|| (cityBuilding.getBlueprint().getBuildingGroup() == BuildingGroup.SHRINE) |
|
) { |
|
|
|
cityBuilding.claim(sourcePlayer); |
|
cityBuilding.setProtectionState(ProtectionState.PROTECTED); |
|
} else if (cityBuilding.getBlueprint().getBuildingGroup() == BuildingGroup.WAREHOUSE) |
|
cityBuilding.claim(sourcePlayer); |
|
|
|
|
|
} |
|
this.setForceRename(true); |
|
CityRecord cityRecord = CityRecord.borrow(this, Enum.RecordEventType.TRANSFER); |
|
DataWarehouse.pushToWarehouse(cityRecord); |
|
return true; |
|
|
|
} |
|
|
|
/** |
|
* @return the forceRename |
|
*/ |
|
public boolean isForceRename() { |
|
return forceRename; |
|
} |
|
|
|
public void setForceRename(boolean forceRename) { |
|
if (!DbManager.CityQueries.updateforceRename(this, forceRename)) |
|
return; |
|
this.forceRename = forceRename; |
|
} |
|
|
|
public String getHash() { |
|
return hash; |
|
} |
|
|
|
public void setHash(String hash) { |
|
this.hash = hash; |
|
} |
|
|
|
public void setHash() { |
|
|
|
this.hash = DataWarehouse.hasher.encrypt(this.getObjectUUID()); |
|
|
|
// Write hash to player character table |
|
|
|
DataWarehouse.writeHash(Enum.DataRecordType.CITY, this.getObjectUUID()); |
|
} |
|
|
|
public boolean setRealmTaxDate(LocalDateTime realmTaxDate) { |
|
|
|
if (!DbManager.CityQueries.updateRealmTaxDate(this, realmTaxDate)) |
|
return false; |
|
|
|
this.realmTaxDate = realmTaxDate; |
|
return true; |
|
|
|
} |
|
|
|
public synchronized boolean TaxWarehouse(TaxResourcesMsg msg, PlayerCharacter player) { |
|
|
|
// Member variable declaration |
|
Building building = BuildingManager.getBuildingFromCache(msg.getBuildingID()); |
|
Guild playerGuild = player.getGuild(); |
|
|
|
if (building == null) { |
|
ErrorPopupMsg.sendErrorMsg(player, "Not a valid Building!"); |
|
return true; |
|
} |
|
|
|
City city = building.getCity(); |
|
|
|
if (city == null) { |
|
ErrorPopupMsg.sendErrorMsg(player, "This building does not belong to a city."); |
|
return true; |
|
} |
|
|
|
if (playerGuild == null || playerGuild.isEmptyGuild()) { |
|
ErrorPopupMsg.sendErrorMsg(player, "You must belong to a guild to do that!"); |
|
return true; |
|
} |
|
|
|
if (playerGuild.getOwnedCity() == null) { |
|
ErrorPopupMsg.sendErrorMsg(player, "Your Guild needs to own a city!"); |
|
return true; |
|
} |
|
|
|
if (playerGuild.getOwnedCity().getTOL() == null) { |
|
ErrorPopupMsg.sendErrorMsg(player, "Cannot find Tree of Life for your city!"); |
|
return true; |
|
} |
|
|
|
if (playerGuild.getOwnedCity().getTOL().getRank() != 8) { |
|
ErrorPopupMsg.sendErrorMsg(player, "Your City needs to Own a realm!"); |
|
return true; |
|
} |
|
|
|
if (playerGuild.getOwnedCity().getRealm() == null) { |
|
ErrorPopupMsg.sendErrorMsg(player, "Cannot find realm for your city!"); |
|
return true; |
|
} |
|
|
|
Realm targetRealm = RealmMap.getRealmForCity(city); |
|
|
|
if (targetRealm == null) { |
|
ErrorPopupMsg.sendErrorMsg(player, "Cannot find realm for city you are attempting to tax!"); |
|
return true; |
|
} |
|
|
|
if (targetRealm.getRulingCity() == null) { |
|
ErrorPopupMsg.sendErrorMsg(player, "Realm Does not have a ruling city!"); |
|
return true; |
|
} |
|
|
|
if (targetRealm.getRulingCity().getObjectUUID() != playerGuild.getOwnedCity().getObjectUUID()) { |
|
ErrorPopupMsg.sendErrorMsg(player, "Your guild does not rule this realm!"); |
|
return true; |
|
} |
|
|
|
if (playerGuild.getOwnedCity().getObjectUUID() == city.getObjectUUID()) { |
|
ErrorPopupMsg.sendErrorMsg(player, "You cannot tax your own city!"); |
|
return true; |
|
} |
|
|
|
|
|
if (!GuildStatusController.isTaxCollector(player.getGuildStatus())) { |
|
ErrorPopupMsg.sendErrorMsg(player, "You Must be a tax Collector!"); |
|
return true; |
|
} |
|
|
|
if (this.realmTaxDate.isAfter(LocalDateTime.now())) |
|
return true; |
|
|
|
if (msg.getResources().size() == 0) |
|
return true; |
|
|
|
if (city.getWarehouse() == null) |
|
return true; |
|
|
|
Warehouse ruledWarehouse = playerGuild.getOwnedCity().getWarehouse(); |
|
|
|
if (ruledWarehouse == null) |
|
return true; |
|
|
|
ItemBase.getItemHashIDMap(); |
|
|
|
ArrayList<Integer> resources = new ArrayList<>(); |
|
|
|
float taxPercent = msg.getTaxPercent(); |
|
|
|
if (taxPercent > 20) |
|
taxPercent = .20f; |
|
|
|
for (int resourceHash : msg.getResources().keySet()) { |
|
if (ItemBase.getItemHashIDMap().get(resourceHash) != null) |
|
resources.add(ItemBase.getItemHashIDMap().get(resourceHash)); |
|
} |
|
|
|
for (Integer itemBaseID : resources) { |
|
ItemBase ib = ItemBase.getItemBase(itemBaseID); |
|
if (ib == null) |
|
continue; |
|
if (ruledWarehouse.isAboveCap(ib, (int) (city.getWarehouse().getResources().get(ib) * taxPercent))) { |
|
ErrorPopupMsg.sendErrorMsg(player, "You're warehouse has enough " + ib.getName() + " already!"); |
|
return true; |
|
} |
|
|
|
} |
|
|
|
if (!city.setRealmTaxDate(LocalDateTime.now().plusDays(7))) { |
|
ErrorPopupMsg.sendErrorMsg(player, "Failed to Update next Tax Date due to internal Error. City was not charged taxes this time."); |
|
return false; |
|
} |
|
|
|
try { |
|
city.getWarehouse().transferResources(player, msg, resources, taxPercent, ruledWarehouse); |
|
} catch (Exception e) { |
|
Logger.info(e.getMessage()); |
|
} |
|
|
|
// Member variable assignment |
|
|
|
ViewResourcesMessage vrm = new ViewResourcesMessage(player); |
|
vrm.setGuild(building.getGuild()); |
|
vrm.setWarehouseBuilding(BuildingManager.getBuildingFromCache(building.getCity().getWarehouse().getBuildingUID())); |
|
vrm.configure(); |
|
Dispatch dispatch = Dispatch.borrow(player, vrm); |
|
DispatchMessage.dispatchMsgDispatch(dispatch, Enum.DispatchChannel.SECONDARY); |
|
dispatch = Dispatch.borrow(player, msg); |
|
DispatchMessage.dispatchMsgDispatch(dispatch, Enum.DispatchChannel.SECONDARY); |
|
return true; |
|
} |
|
}
|
|
|