Commands overhaul + Selective loot + Antimulticlient Coordinator

Completely overhauled commands layout, each command splitted in Java classes.
Optimized "ranks" command, no more calling the DB to get ranking info.
Implemented a mechanic where mobs only spawns loots that are visible/collectable by the player's party.
Implemented a server flag which sets whether explorers, cygnus and legends are allowed to share the cash shop inventory or not.
Implemented support for dynamic server rates at bootup. Rates can now be assigned at the configuration.ini file.
Devised the anti-multiclient login coordinator feature. Besides multiclient attempts by the same machine, it also prevents unauthorized login attempts into an account.
Fixed PQ instances being forcefully closed even when party leader reassignment upon logout is available.
Fixed mob statis not concurrently protected.
This commit is contained in:
ronancpl
2018-08-28 14:12:00 -03:00
parent b5c6831129
commit 132f286391
210 changed files with 10518 additions and 3851 deletions
+11 -6
View File
@@ -46,6 +46,7 @@ import client.inventory.ItemFactory;
import client.inventory.MapleInventoryType;
import client.inventory.MaplePet;
import constants.ItemConstants;
import constants.ServerConstants;
import java.util.Collections;
import net.server.audit.locks.MonitoredLockType;
@@ -275,12 +276,16 @@ public class CashShop {
this.accountId = accountId;
this.characterId = characterId;
if (jobType == 0) {
factory = ItemFactory.CASH_EXPLORER;
} else if (jobType == 1) {
factory = ItemFactory.CASH_CYGNUS;
} else if (jobType == 2) {
factory = ItemFactory.CASH_ARAN;
if (!ServerConstants.USE_JOINT_CASHSHOP_INVENTORY) {
if (jobType == 0) {
factory = ItemFactory.CASH_EXPLORER;
} else if (jobType == 1) {
factory = ItemFactory.CASH_CYGNUS;
} else if (jobType == 2) {
factory = ItemFactory.CASH_ARAN;
}
} else {
factory = ItemFactory.CASH_OVERALL;
}
Connection con = DatabaseConnection.getConnection();
+18 -16
View File
@@ -77,8 +77,12 @@ import tools.StringUtil;
*
*/
public class MapleItemInformationProvider {
private static MapleItemInformationProvider instance = null;
private final static MapleItemInformationProvider instance = new MapleItemInformationProvider();
public static MapleItemInformationProvider getInstance() {
return instance;
}
protected MapleDataProvider itemData;
protected MapleDataProvider equipData;
protected MapleDataProvider stringData;
@@ -142,13 +146,6 @@ public class MapleItemInformationProvider {
isPartyQuestItemCache.put(0, false);
}
public static MapleItemInformationProvider getInstance() {
if (instance == null) {
instance = new MapleItemInformationProvider();
}
return instance;
}
// public MapleInventoryType getInventoryType(int itemId) {
// if (inventoryTypeCache.containsKey(itemId)) {
// return inventoryTypeCache.get(itemId);
@@ -1224,14 +1221,19 @@ public class MapleItemInformationProvider {
if (dropRestrictionCache.containsKey(itemId)) {
return dropRestrictionCache.get(itemId);
}
MapleData data = getItemData(itemId);
boolean bRestricted = MapleDataTool.getIntConvert("info/tradeBlock", data, 0) == 1;
if (!bRestricted) {
bRestricted = MapleDataTool.getIntConvert("info/accountSharable", data, 0) == 1;
}
if (!bRestricted) {
bRestricted = MapleDataTool.getIntConvert("info/quest", data, 0) == 1;
boolean bRestricted = false;
if(itemId != 0) {
MapleData data = getItemData(itemId);
bRestricted = MapleDataTool.getIntConvert("info/tradeBlock", data, 0) == 1;
if (!bRestricted) {
bRestricted = MapleDataTool.getIntConvert("info/accountSharable", data, 0) == 1;
}
if (!bRestricted) {
bRestricted = MapleDataTool.getIntConvert("info/quest", data, 0) == 1;
}
}
dropRestrictionCache.put(itemId, bRestricted);
return bRestricted;
}
+5 -3
View File
@@ -29,13 +29,15 @@ import java.util.Map;
* @author Matze
*/
public class MapleShopFactory {
private Map<Integer, MapleShop> shops = new HashMap<Integer, MapleShop>();
private Map<Integer, MapleShop> npcShops = new HashMap<Integer, MapleShop>();
private static MapleShopFactory instance = new MapleShopFactory();
public static MapleShopFactory getInstance() {
return instance;
}
private Map<Integer, MapleShop> shops = new HashMap<Integer, MapleShop>();
private Map<Integer, MapleShop> npcShops = new HashMap<Integer, MapleShop>();
private MapleShop loadShop(int id, boolean isShopId) {
MapleShop ret = MapleShop.createFromDB(id, isShopId);
+5 -4
View File
@@ -34,6 +34,11 @@ import tools.FilePrinter;
public class TimerManager implements TimerManagerMBean {
private static TimerManager instance = new TimerManager();
public static TimerManager getInstance() {
return instance;
}
private ScheduledThreadPoolExecutor ses;
private TimerManager() {
@@ -45,10 +50,6 @@ public class TimerManager implements TimerManagerMBean {
}
}
public static TimerManager getInstance() {
return instance;
}
public void start() {
if (ses != null && !ses.isShutdown() && !ses.isTerminated()) {
return;
+4 -4
View File
@@ -30,6 +30,10 @@ import tools.Randomizer;
public class MapleGachapon {
private static final MapleGachapon instance = new MapleGachapon();
public static MapleGachapon getInstance() {
return instance;
}
public enum Gachapon {
@@ -89,10 +93,6 @@ public class MapleGachapon {
return null;
}
}
public static MapleGachapon getInstance() {
return instance;
}
public MapleGachaponItem process(int npcId) {
Gachapon gacha = Gachapon.getByNpcId(npcId);
+14 -9
View File
@@ -505,7 +505,7 @@ public class MapleMonster extends AbstractLoadedMapleLife {
}
if(!expDist.isEmpty()) { // locate on world server the partyid of the missing characters
World wserv = Server.getInstance().getWorld(map.getWorld());
World wserv = map.getWorldServer();
for (Entry<Integer, Float> ed : expDist.entrySet()) {
boolean isKiller = (ed.getKey() == killerId);
@@ -1151,16 +1151,21 @@ public class MapleMonster extends AbstractLoadedMapleLife {
}
private void debuffMobStat(MonsterStatus stat) {
if (isBuffed(stat)) {
final MonsterStatusEffect oldEffect = stati.get(stat);
byte[] packet = MaplePacketCreator.cancelMonsterStatus(getObjectId(), oldEffect.getStati());
map.broadcastMessage(packet, getPosition());
statiLock.lock();
try {
if (isBuffed(stat)) {
final MonsterStatusEffect oldEffect = stati.get(stat);
byte[] packet = MaplePacketCreator.cancelMonsterStatus(getObjectId(), oldEffect.getStati());
map.broadcastMessage(packet, getPosition());
MapleCharacter chrController = getController();
if (chrController != null && !chrController.isMapObjectVisible(MapleMonster.this)) {
chrController.getClient().announce(packet);
MapleCharacter chrController = getController();
if (chrController != null && !chrController.isMapObjectVisible(MapleMonster.this)) {
chrController.getClient().announce(packet);
}
stati.remove(stat);
}
stati.remove(stat);
} finally {
statiLock.unlock();
}
}
@@ -47,6 +47,11 @@ public class MapleMonsterInformationProvider {
// Author : LightPepsi
private static final MapleMonsterInformationProvider instance = new MapleMonsterInformationProvider();
public static MapleMonsterInformationProvider getInstance() {
return instance;
}
private final Map<Integer, List<MonsterDropEntry>> drops = new HashMap<>();
private final List<MonsterGlobalDropEntry> globaldrops = new ArrayList<>();
@@ -61,10 +66,6 @@ public class MapleMonsterInformationProvider {
retrieveGlobal();
}
public static MapleMonsterInformationProvider getInstance() {
return instance;
}
public final List<MonsterGlobalDropEntry> getGlobalDrop() {
return globaldrops;
}
@@ -138,16 +138,16 @@ public class MaplePlayerNPCPodium {
if(podiumStep >= ServerConstants.PLAYERNPC_AREA_STEPS) return null;
List<MapleMapObject> mmoList = map.getMapObjectsInRange(new Point(0, 0), Double.POSITIVE_INFINITY, Arrays.asList(MapleMapObjectType.PLAYER_NPC));
Server.getInstance().getWorld(map.getWorld()).setPlayerNpcMapPodiumData(map.getId(), encodePodiumData(podiumStep + 1, podiumCount + 1));
map.getWorldServer().setPlayerNpcMapPodiumData(map.getId(), encodePodiumData(podiumStep + 1, podiumCount + 1));
return reorganizePlayerNpcs(map, podiumStep + 1, mmoList);
} else {
Server.getInstance().getWorld(map.getWorld()).setPlayerNpcMapPodiumData(map.getId(), encodePodiumData(podiumStep, podiumCount + 1));
map.getWorldServer().setPlayerNpcMapPodiumData(map.getId(), encodePodiumData(podiumStep, podiumCount + 1));
return calcNextPos(podiumCount, podiumStep);
}
}
public static Point getNextPlayerNpcPosition(MapleMap map) {
Point pos = getNextPlayerNpcPosition(map, Server.getInstance().getWorld(map.getWorld()).getPlayerNpcMapPodiumData(map.getId()));
Point pos = getNextPlayerNpcPosition(map, map.getWorldServer().getPlayerNpcMapPodiumData(map.getId()));
if(pos == null) return null;
return map.getGroundBelow(pos);
@@ -204,7 +204,7 @@ public class MaplePlayerNPCPositioner {
if(searchPos != null) {
if(!isPlayerNpcNearby(otherPlayerNpcs, searchPos, cx, cy)) {
if(i > initStep) {
Server.getInstance().getWorld(map.getWorld()).setPlayerNpcMapStep(map.getId(), i);
map.getWorldServer().setPlayerNpcMapStep(map.getId(), i);
}
if(reorganize && ServerConstants.PLAYERNPC_ORGANIZE_AREA) {
@@ -232,12 +232,12 @@ public class MaplePlayerNPCPositioner {
}
if(i > initStep) {
Server.getInstance().getWorld(map.getWorld()).setPlayerNpcMapStep(map.getId(), ServerConstants.PLAYERNPC_AREA_STEPS - 1);
map.getWorldServer().setPlayerNpcMapStep(map.getId(), ServerConstants.PLAYERNPC_AREA_STEPS - 1);
}
return null;
}
public static Point getNextPlayerNpcPosition(MapleMap map) {
return getNextPlayerNpcPosition(map, Server.getInstance().getWorld(map.getWorld()).getPlayerNpcMapStep(map.getId()));
return getNextPlayerNpcPosition(map, map.getWorldServer().getPlayerNpcMapStep(map.getId()));
}
}
+56
View File
@@ -0,0 +1,56 @@
/*
This file is part of the HeavenMS MapleStory Server
Copyleft (L) 2016 - 2018 RonanLana
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation version 3 as published by
the Free Software Foundation. You may not use, modify or distribute
this program under any other version of the GNU Affero General Public
License.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package server.loot;
import client.MapleCharacter;
import java.util.HashMap;
import java.util.Map;
import client.inventory.Item;
import client.inventory.MapleInventoryType;
/**
*
* @author Ronan
*/
public class MapleLootInventory {
Map<Integer, Integer> items = new HashMap<>(50);
public MapleLootInventory(MapleCharacter from) {
for (MapleInventoryType values : MapleInventoryType.values()) {
for(Item it : from.getInventory(values).list()) {
Integer itemQty = items.get(it.getItemId());
if(itemQty == null) {
items.put(it.getItemId(), (int) it.getQuantity());
} else {
items.put(it.getItemId(), itemQty + it.getQuantity());
}
}
}
}
public boolean hasItem(int itemid, int quantity) {
Integer itemQty = items.get(itemid);
return itemQty != null && itemQty >= quantity;
}
}
+81
View File
@@ -0,0 +1,81 @@
/*
This file is part of the HeavenMS MapleStory Server
Copyleft (L) 2016 - 2018 RonanLana
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation version 3 as published by
the Free Software Foundation. You may not use, modify or distribute
this program under any other version of the GNU Affero General Public
License.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package server.loot;
import client.MapleCharacter;
import java.util.LinkedList;
import java.util.List;
import server.MapleItemInformationProvider;
import server.life.MapleMonsterInformationProvider;
import server.life.MonsterDropEntry;
import server.quest.MapleQuest;
/**
*
* @author Ronan
*/
public class MapleLootManager {
private static boolean needQuestItem(MonsterDropEntry dropEntry, MapleQuest quest, MapleCharacter chr, MapleLootInventory chrInv) {
if (chr.getQuestStatus(dropEntry.questid) != 1) return false;
return !chrInv.hasItem(dropEntry.itemId, quest.getItemAmountNeeded(dropEntry.itemId));
}
private static boolean isRelevantDrop(MonsterDropEntry dropEntry, List<MapleCharacter> partyMembers, List<MapleLootInventory> partyInv) {
MapleQuest quest = MapleQuest.getInstance(dropEntry.questid);
boolean restricted = MapleItemInformationProvider.getInstance().isDropRestricted(dropEntry.itemId);
for (int i = 0; i < partyMembers.size(); i++) {
MapleLootInventory chrInv = partyInv.get(i);
if(restricted && chrInv.hasItem(dropEntry.itemId, 1)) {
continue;
}
if(dropEntry.questid <= 0 || needQuestItem(dropEntry, quest, partyMembers.get(i), chrInv)) {
return true;
}
}
return false;
}
public static List<MonsterDropEntry> retrieveRelevantDrops(int monsterId, List<MapleCharacter> partyMembers) {
List<MonsterDropEntry> loots = MapleMonsterInformationProvider.getInstance().retrieveEffectiveDrop(monsterId);
if(loots.isEmpty()) return loots;
List<MapleLootInventory> partyInv = new LinkedList<>();
for(MapleCharacter chr : partyMembers) {
MapleLootInventory lootInv = new MapleLootInventory(chr);
partyInv.add(lootInv);
}
List<MonsterDropEntry> effectiveLoot = new LinkedList<>();
for(MonsterDropEntry mde : loots) {
if(isRelevantDrop(mde, partyMembers, partyInv)) {
effectiveLoot.add(mde);
}
}
return effectiveLoot;
}
}
+20 -20
View File
@@ -63,6 +63,7 @@ import net.server.audit.locks.factory.MonitoredReentrantLockFactory;
import java.lang.ref.WeakReference;
import net.server.Server;
import net.server.channel.Channel;
import net.server.world.World;
import scripting.map.MapScriptManager;
import server.MapleItemInformationProvider;
import server.MaplePortal;
@@ -84,7 +85,6 @@ import server.life.SpawnPoint;
import server.partyquest.MonsterCarnival;
import server.partyquest.MonsterCarnivalParty;
//import server.partyquest.Pyramid;
import scripting.event.EventManager;
import scripting.event.EventInstanceManager;
import server.life.MaplePlayerNPC;
import server.life.MonsterListener;
@@ -262,6 +262,10 @@ public class MapleMap {
public Channel getChannelServer() {
return Server.getInstance().getWorld(world).getChannel(channel);
}
public World getWorldServer() {
return Server.getInstance().getWorld(world);
}
public MapleMap getReturnMap() {
if(returnMapId == 999999999) return this;
@@ -697,7 +701,7 @@ public class MapleMap {
final List<MonsterDropEntry> dropEntry = new ArrayList<>();
final List<MonsterDropEntry> visibleQuestEntry = new ArrayList<>();
final List<MonsterDropEntry> otherQuestEntry = new ArrayList<>();
sortDropEntries(mi.retrieveEffectiveDrop(mob.getId()), dropEntry, visibleQuestEntry, otherQuestEntry, chr);
sortDropEntries(ServerConstants.USE_SPAWN_RELEVANT_LOOT ? chr.retrieveRelevantDrops(mob.getId()) : mi.retrieveEffectiveDrop(mob.getId()), dropEntry, visibleQuestEntry, otherQuestEntry, chr);
registerMobItemDrops(droptype, mobpos, chRate, pos, dropEntry, visibleQuestEntry, otherQuestEntry, globalEntry, chr, mob);
}
@@ -974,10 +978,6 @@ public class MapleMap {
unregisterItemDrop(mdrop);
}
private static boolean shouldShowQuestItem(MapleCharacter chr, int questid, int itemid) {
return questid <= 0 || chr.needQuestItem(questid, itemid);
}
public void updatePlayerItemDrops(int partyid, int charid, List<MapleCharacter> partyMembers, MapleCharacter partyLeaver) {
for(MapleMapItem mdrop : getDroppedItems()) {
if(mdrop.getOwnerId() == charid) {
@@ -994,7 +994,7 @@ public class MapleMap {
if(this.equals(mc.getMap())) {
mc.announce(removePacket);
if(shouldShowQuestItem(mc, mdrop.getQuest(), mdrop.getItemId())) {
if(mc.needQuestItem(mdrop.getQuest(), mdrop.getItemId())) {
mc.announce(updatePacket);
}
}
@@ -1004,7 +1004,7 @@ public class MapleMap {
if(this.equals(partyLeaver.getMap())) {
partyLeaver.announce(removePacket);
if(shouldShowQuestItem(partyLeaver, mdrop.getQuest(), mdrop.getItemId())) {
if(partyLeaver.needQuestItem(mdrop.getQuest(), mdrop.getItemId())) {
partyLeaver.announce(MaplePacketCreator.updateMapItemObject(mdrop, true));
}
}
@@ -1024,7 +1024,7 @@ public class MapleMap {
public void sendPackets(MapleClient c) {
MapleCharacter chr = c.getPlayer();
if (shouldShowQuestItem(chr, questid, idrop.getItemId())) {
if (chr.needQuestItem(questid, idrop.getItemId())) {
mdrop.lockItem();
try {
c.announce(MaplePacketCreator.dropItemFromMapObject(chr.getParty() != null, mdrop, dropper.getPosition(), dropPos, (byte) 1));
@@ -1208,19 +1208,19 @@ public class MapleMap {
}
public void broadcastBalrogVictory(String leaderName) {
Server.getInstance().getWorld(world).dropMessage(6, "[VICTORY] " + leaderName + "'s party has successfully defeated the Balrog! Praise to them, they finished with " + countAlivePlayers() + " players alive.");
getWorldServer().dropMessage(6, "[VICTORY] " + leaderName + "'s party has successfully defeated the Balrog! Praise to them, they finished with " + countAlivePlayers() + " players alive.");
}
public void broadcastHorntailVictory() {
Server.getInstance().getWorld(world).dropMessage(6, "[VICTORY] To the crew that have finally conquered Horned Tail after numerous attempts, I salute thee! You are the true heroes of Leafre!!");
getWorldServer().dropMessage(6, "[VICTORY] To the crew that have finally conquered Horned Tail after numerous attempts, I salute thee! You are the true heroes of Leafre!!");
}
public void broadcastZakumVictory() {
Server.getInstance().getWorld(world).dropMessage(6, "[VICTORY] At last, the tree of evil that for so long overwhelmed Ossyria has fallen. To the crew that managed to finally conquer Zakum, after numerous attempts, victory! You are the true heroes of Ossyria!!");
getWorldServer().dropMessage(6, "[VICTORY] At last, the tree of evil that for so long overwhelmed Ossyria has fallen. To the crew that managed to finally conquer Zakum, after numerous attempts, victory! You are the true heroes of Ossyria!!");
}
public void broadcastPinkBeanVictory(int channel) {
Server.getInstance().getWorld(world).dropMessage(6, "[VICTORY] In a swift stroke of sorts, the crew that has attempted Pink Bean at channel " + channel + " has ultimately defeated it. The Temple of Time shines radiantly once again, the day finally coming back, as the crew that managed to finally conquer it returns victoriously from the battlefield!!");
getWorldServer().dropMessage(6, "[VICTORY] In a swift stroke of sorts, the crew that has attempted Pink Bean at channel " + channel + " has ultimately defeated it. The Temple of Time shines radiantly once again, the day finally coming back, as the crew that managed to finally conquer it returns victoriously from the battlefield!!");
}
private boolean removeKilledMonsterObject(MapleMonster monster) {
@@ -2089,7 +2089,7 @@ public class MapleMap {
}
};
Server.getInstance().getWorld(world).registerTimedMapObject(expireKite, ServerConstants.KITE_EXPIRE_TIME);
getWorldServer().registerTimedMapObject(expireKite, ServerConstants.KITE_EXPIRE_TIME);
}
public final void spawnItemDrop(final MapleMapObject dropper, final MapleCharacter owner, final Item item, Point pos, final boolean ffaDrop, final boolean playerDrop) {
@@ -2360,7 +2360,7 @@ public class MapleMap {
spawnMonsterOnGroundBelow(MapleLifeFactory.getMonster(9300102), new Point(77, 426));
}
} else if (mapid == 200090060) { // To Rien
int travelTime = EventManager.getTransportationTime(1 * 60 * 1000);
int travelTime = getWorldServer().getTransportationTime(1 * 60 * 1000);
chr.announce(MaplePacketCreator.getClock(travelTime / 1000));
TimerManager.getInstance().schedule(new Runnable() {
@@ -2372,7 +2372,7 @@ public class MapleMap {
}
}, travelTime);
} else if (mapid == 200090070) { // To Lith Harbor
int travelTime = EventManager.getTransportationTime(1 * 60 * 1000);
int travelTime = getWorldServer().getTransportationTime(1 * 60 * 1000);
chr.announce(MaplePacketCreator.getClock(travelTime / 1000));
TimerManager.getInstance().schedule(new Runnable() {
@@ -2384,7 +2384,7 @@ public class MapleMap {
}
}, travelTime);
} else if (mapid == 200090030) { // To Ereve (SkyFerry)
int travelTime = EventManager.getTransportationTime(2 * 60 * 1000);
int travelTime = getWorldServer().getTransportationTime(2 * 60 * 1000);
chr.announce(MaplePacketCreator.getClock(travelTime / 1000));
TimerManager.getInstance().schedule(new Runnable() {
@@ -2396,7 +2396,7 @@ public class MapleMap {
}
}, travelTime);
} else if (mapid == 200090031) { // To Victoria Island (SkyFerry)
int travelTime = EventManager.getTransportationTime(2 * 60 * 1000);
int travelTime = getWorldServer().getTransportationTime(2 * 60 * 1000);
chr.announce(MaplePacketCreator.getClock(travelTime / 1000));
TimerManager.getInstance().schedule(new Runnable() {
@@ -2408,7 +2408,7 @@ public class MapleMap {
}
}, travelTime);
} else if (mapid == 200090021) { // To Orbis (SkyFerry)
int travelTime = EventManager.getTransportationTime(8 * 60 * 1000);
int travelTime = getWorldServer().getTransportationTime(8 * 60 * 1000);
chr.announce(MaplePacketCreator.getClock(travelTime / 1000));
TimerManager.getInstance().schedule(new Runnable() {
@@ -2420,7 +2420,7 @@ public class MapleMap {
}
}, travelTime);
} else if (mapid == 200090020) { // To Ereve From Orbis (SkyFerry)
int travelTime = EventManager.getTransportationTime(8 * 60 * 1000);
int travelTime = getWorldServer().getTransportationTime(8 * 60 * 1000);
chr.announce(MaplePacketCreator.getClock(travelTime / 1000));
TimerManager.getInstance().schedule(new Runnable() {
+1 -1
View File
@@ -184,7 +184,7 @@ public class MapleMapItem extends AbstractMapleMapObject {
public void sendSpawnData(final MapleClient client) {
MapleCharacter chr = client.getPlayer();
if (questid <= 0 || chr.needQuestItem(questid, item.getItemId())) {
if (chr.needQuestItem(questid, getItemId())) {
this.lockItem();
try {
client.announce(MaplePacketCreator.dropItemFromMapObject(chr.getParty() != null, this, null, getPosition(), (byte) 2));