CPQ 1 CPQ 2 CASAMENTO

This commit is contained in:
Diego Armando de Freitas Matos
2019-03-06 21:55:47 -03:00
parent 799870df63
commit 90ad58f17f
60 changed files with 17440 additions and 15140 deletions
File diff suppressed because it is too large Load Diff
+75 -40
View File
@@ -48,12 +48,14 @@ import server.life.MapleMonster;
import server.life.MaplePlayerNPC;
import server.life.MaplePlayerNPCFactory;
import scripting.event.EventInstanceManager;
import server.partyquest.GuardianSpawnPoint;
import tools.DatabaseConnection;
import tools.StringUtil;
public class MapleMapFactory {
private static Map<Integer, Float> mapRecoveryRate = new HashMap<>();
private MapleDataProvider source;
private MapleData nameData;
private EventInstanceManager event;
@@ -68,12 +70,12 @@ public class MapleMapFactory {
this.world = world;
this.channel = channel;
this.event = eim;
ReentrantReadWriteLock rrwl = new MonitoredReentrantReadWriteLock(MonitoredLockType.MAP_FACTORY);
this.mapsRLock = rrwl.readLock();
this.mapsWLock = rrwl.writeLock();
}
public MapleMap resetMap(int mapid) {
mapsWLock.lock();
try {
@@ -81,14 +83,23 @@ public class MapleMapFactory {
} finally {
mapsWLock.unlock();
}
return getMap(mapid);
}
private void loadLifeFromWz(MapleMap map, MapleData mapData) {
for (MapleData life : mapData.getChildByPath("life")) {
life.getName();
String id = MapleDataTool.getString(life.getChildByPath("id"));
String type = MapleDataTool.getString(life.getChildByPath("type"));
int team = MapleDataTool.getInt("team", life, -1);
if (map.isCPQMap2() && type.equals("m")) {
if((Integer.parseInt(life.getName()) % 2) == 0) {
team = 0;
} else {
team = 1;
}
}
int cy = MapleDataTool.getInt(life.getChildByPath("cy"));
MapleData dF = life.getChildByPath("f");
int f = (dF != null) ? MapleDataTool.getInt(dF) : 0;
@@ -99,12 +110,11 @@ public class MapleMapFactory {
int y = MapleDataTool.getInt(life.getChildByPath("y"));
int hide = MapleDataTool.getInt("hide", life, 0);
int mobTime = MapleDataTool.getInt("mobTime", life, 0);
int team = MapleDataTool.getInt("team", life, -1);
loadLifeRaw(map, Integer.parseInt(id), type, cy, f, fh, rx0, rx1, x, y, hide, mobTime, team);
}
}
private void loadLifeFromDb(MapleMap map) {
try {
Connection con = DatabaseConnection.getConnection();
@@ -113,7 +123,7 @@ public class MapleMapFactory {
ps.setInt(2, map.getWorld());
ResultSet rs = ps.executeQuery();
while(rs.next()) {
while (rs.next()) {
int id = rs.getInt("life");
String type = rs.getString("type");
int cy = rs.getInt("cy");
@@ -137,12 +147,12 @@ public class MapleMapFactory {
sqle.printStackTrace();
}
}
private void loadLifeRaw(MapleMap map, int id, String type, int cy, int f, int fh, int rx0, int rx1, int x, int y, int hide, int mobTime, int team) {
AbstractLoadedMapleLife myLife = loadLife(id, type, cy, f, fh, rx0, rx1, x, y, hide);
if (myLife instanceof MapleMonster) {
MapleMonster monster = (MapleMonster) myLife;
if (mobTime == -1) { //does not respawn, force spawn once
map.spawnMonster(monster);
} else {
@@ -155,10 +165,10 @@ public class MapleMapFactory {
map.addMapObject(myLife);
}
}
private synchronized MapleMap loadMapFromWz(int mapid, Integer omapid) {
MapleMap map;
mapsRLock.lock();
try {
map = maps.get(omapid);
@@ -169,7 +179,7 @@ public class MapleMapFactory {
if (map != null) {
return map;
}
String mapName = getMapName(mapid);
MapleData mapData = source.getData(mapName); // source.getData issue with giving nulls in rare ocasions found thanks to MedicOP
MapleData infoData = mapData.getChildByPath("info");
@@ -209,9 +219,9 @@ public class MapleMapFactory {
bounds[0] = MapleDataTool.getInt(infoData.getChildByPath("VRTop"));
bounds[1] = MapleDataTool.getInt(infoData.getChildByPath("VRBottom"));
if(bounds[0] == bounds[1]) { // old-style baked map
if (bounds[0] == bounds[1]) { // old-style baked map
MapleData minimapData = mapData.getChildByPath("miniMap");
if(minimapData != null) {
if (minimapData != null) {
bounds[0] = MapleDataTool.getInt(minimapData.getChildByPath("centerX")) * -1;
bounds[1] = MapleDataTool.getInt(minimapData.getChildByPath("centerY")) * -1;
bounds[2] = MapleDataTool.getInt(minimapData.getChildByPath("height"));
@@ -272,7 +282,7 @@ public class MapleMapFactory {
map.addMapleArea(new Rectangle(x1, y1, (x2 - x1), (y2 - y1)));
}
}
if(event == null) {
if (event == null) {
try {
Connection con = DatabaseConnection.getConnection();
try (PreparedStatement ps = con.prepareStatement("SELECT * FROM playernpcs WHERE map = ? AND world = ?")) {
@@ -288,23 +298,47 @@ public class MapleMapFactory {
} catch (SQLException e) {
e.printStackTrace();
}
List<MaplePlayerNPC> dnpcs = MaplePlayerNPCFactory.getDeveloperNpcsFromMapid(mapid);
if(dnpcs != null) {
for(MaplePlayerNPC dnpc : dnpcs) {
if (dnpcs != null) {
for (MaplePlayerNPC dnpc : dnpcs) {
map.addPlayerNPCMapObject(dnpc);
}
}
}
loadLifeFromWz(map, mapData);
loadLifeFromDb(map);
if (map.isCPQMap()) {
MapleData mcData = mapData.getChildByPath("monsterCarnival");
if (mcData != null) {
MapleData guardianGenData = mcData.getChildByPath("guardianGenPos");
for (MapleData node : guardianGenData.getChildren()) {
GuardianSpawnPoint pt = new GuardianSpawnPoint(new Point(MapleDataTool.getIntConvert("x", node), MapleDataTool.getIntConvert("y", node)));
pt.setTeam(MapleDataTool.getIntConvert("team", node, -1));
pt.setTaken(false);
map.addGuardianSpawnPoint(pt);
}
}
if (mcData.getChildByPath("skill") != null) {
for (MapleData area : mcData.getChildByPath("skill")) {
map.addSkillId(MapleDataTool.getInt(area));
}
}
if (mcData.getChildByPath("mob") != null) {
for (MapleData area : mcData.getChildByPath("mob")) {
map.addMobSpawn(MapleDataTool.getInt(area.getChildByPath("id")), MapleDataTool.getInt(area.getChildByPath("spendCP")));
}
}
}
if (mapData.getChildByPath("reactor") != null) {
for (MapleData reactor : mapData.getChildByPath("reactor")) {
String id = MapleDataTool.getString(reactor.getChildByPath("id"));
if (id != null) {
MapleReactor newReactor = loadReactor(reactor, id);
MapleReactor newReactor = loadReactor(reactor, id, (byte) MapleDataTool.getInt(reactor.getChildByPath("f"), 0));
map.spawnReactor(newReactor);
}
}
@@ -313,7 +347,7 @@ public class MapleMapFactory {
map.setMapName(MapleDataTool.getString("mapName", nameData.getChildByPath(getMapStringName(omapid)), ""));
map.setStreetName(MapleDataTool.getString("streetName", nameData.getChildByPath(getMapStringName(omapid)), ""));
} catch (Exception e) {
if(omapid / 1000 != 1020) { // explorer job introducion scenes
if (omapid / 1000 != 1020) { // explorer job introducion scenes
e.printStackTrace();
System.err.println("Not found mapid " + omapid);
}
@@ -332,13 +366,13 @@ public class MapleMapFactory {
map.setTimeLimit(MapleDataTool.getIntConvert("timeLimit", infoData, -1));
map.setFieldType(MapleDataTool.getIntConvert("fieldType", infoData, 0));
map.setMobCapacity(MapleDataTool.getIntConvert("fixedMobCapacity", infoData, 500));//Is there a map that contains more than 500 mobs?
MapleData recData = infoData.getChildByPath("recovery");
if(recData != null) {
if (recData != null) {
float recoveryRate = MapleDataTool.getFloat(recData);
mapRecoveryRate.put(mapid, recoveryRate);
}
HashMap<Integer, Integer> backTypes = new HashMap<>();
try {
for (MapleData layer : mapData.getChildByPath("back")) { // yolo
@@ -351,7 +385,7 @@ public class MapleMapFactory {
e.printStackTrace();
// swallow cause I'm cool
}
map.setBackgroundTypes(backTypes);
map.generateMapDropRangeCache();
@@ -361,21 +395,21 @@ public class MapleMapFactory {
} finally {
mapsWLock.unlock();
}
return map;
}
public MapleMap getMap(int mapid) {
Integer omapid = Integer.valueOf(mapid);
MapleMap map;
mapsRLock.lock();
try {
map = maps.get(omapid);
} finally {
mapsRLock.unlock();
}
return (map != null) ? map : loadMapFromWz(mapid, omapid);
}
@@ -387,7 +421,7 @@ public class MapleMapFactory {
mapsRLock.unlock();
}
}
private AbstractLoadedMapleLife loadLife(int id, String type, int cy, int f, int fh, int rx0, int rx1, int x, int y, int hide) {
AbstractLoadedMapleLife myLife = MapleLifeFactory.getLife(id, type);
myLife.setCy(cy);
@@ -402,10 +436,11 @@ public class MapleMapFactory {
return myLife;
}
private MapleReactor loadReactor(MapleData reactor, String id) {
private MapleReactor loadReactor(MapleData reactor, String id, final byte FacingDirection) {
MapleReactor myReactor = new MapleReactor(MapleReactorFactory.getReactor(Integer.parseInt(id)), Integer.parseInt(id));
int x = MapleDataTool.getInt(reactor.getChildByPath("x"));
int y = MapleDataTool.getInt(reactor.getChildByPath("y"));
myReactor.setFacingDirection(FacingDirection);
myReactor.setPosition(new Point(x, y));
myReactor.setDelay(MapleDataTool.getInt(reactor.getChildByPath("reactorTime")) * 1000);
myReactor.setName(MapleDataTool.getString(reactor.getChildByPath("name"), ""));
@@ -442,7 +477,7 @@ public class MapleMapFactory {
} else if (mapid >= 677000000 && mapid < 677100000) {
builder.append("Episode1GL");
} else if (mapid >= 670000000 && mapid < 682000000) {
if((mapid >= 674030000 && mapid < 674040000) || (mapid >= 680100000 && mapid < 680200000)) {
if ((mapid >= 674030000 && mapid < 674040000) || (mapid >= 680100000 && mapid < 680200000)) {
builder.append("etc");
} else {
builder.append("weddingGL");
@@ -452,7 +487,7 @@ public class MapleMapFactory {
} else if (mapid >= 683000000 && mapid < 684000000) {
builder.append("event");
} else if (mapid >= 800000000 && mapid < 900000000) {
if((mapid >= 889100000 && mapid < 889200000)) {
if ((mapid >= 889100000 && mapid < 889200000)) {
builder.append("etc");
} else {
builder.append("jp");
@@ -480,17 +515,17 @@ public class MapleMapFactory {
mapsRLock.unlock();
}
}
public void dispose() {
Collection<MapleMap> mapValues = getMaps().values();
for(MapleMap map: mapValues) {
for (MapleMap map : mapValues) {
map.dispose();
}
this.event = null;
}
public static float getMapRecoveryRate(int mapid) {
Float recRate = mapRecoveryRate.get(mapid);
return recRate != null ? recRate : 1.0f;
+81 -50
View File
@@ -1,27 +1,28 @@
/*
This file is part of the OdinMS Maple Story Server
Copyright (C) 2008 Patrick Huy <patrick.huy@frz.cc>
Matthias Butz <matze@odinms.de>
Jan Christian Meyer <vimes@odinms.de>
This file is part of the OdinMS Maple Story Server
Copyright (C) 2008 Patrick Huy <patrick.huy@frz.cc>
Matthias Butz <matze@odinms.de>
Jan Christian Meyer <vimes@odinms.de>
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 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.
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/>.
*/
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.maps;
import client.MapleClient;
import client.status.MonsterStatus;
import constants.ServerConstants;
import java.awt.Rectangle;
@@ -36,6 +37,7 @@ import server.TimerManager;
import tools.MaplePacketCreator;
import tools.Pair;
import net.server.audit.locks.MonitoredLockType;
import server.partyquest.GuardianSpawnPoint;
/**
*
@@ -43,6 +45,7 @@ import net.server.audit.locks.MonitoredLockType;
* @author Ronan
*/
public class MapleReactor extends AbstractMapleMapObject {
private int rid;
private MapleReactorStats stats;
private byte state;
@@ -54,28 +57,30 @@ public class MapleReactor extends AbstractMapleMapObject {
private boolean shouldCollect;
private boolean attackHit;
private ScheduledFuture<?> timeoutTask = null;
private GuardianSpawnPoint guardian = null;
private byte facingDirection = 0;
private Lock reactorLock = MonitoredReentrantLockFactory.createLock(MonitoredLockType.REACTOR, true);
private Lock hitLock = MonitoredReentrantLockFactory.createLock(MonitoredLockType.REACTOR_HIT, true);
public MapleReactor(MapleReactorStats stats, int rid) {
this.evstate = (byte)0;
this.evstate = (byte) 0;
this.stats = stats;
this.rid = rid;
this.alive = true;
}
public void setShouldCollect(boolean collect) {
this.shouldCollect = collect;
}
public boolean getShouldCollect() {
return shouldCollect;
}
public void lockReactor() {
reactorLock.lock();
}
public void unlockReactor() {
reactorLock.unlock();
}
@@ -83,19 +88,19 @@ public class MapleReactor extends AbstractMapleMapObject {
public void setState(byte state) {
this.state = state;
}
public byte getState() {
return state;
}
public void setEventState(byte substate) {
this.evstate = substate;
}
public byte getEventState() {
return evstate;
}
public MapleReactorStats getStats() {
return stats;
}
@@ -120,7 +125,7 @@ public class MapleReactor extends AbstractMapleMapObject {
public int getReactorType() {
return stats.getType(state);
}
public boolean isRecentHitFromAttack() {
return attackHit;
}
@@ -140,7 +145,7 @@ public class MapleReactor extends AbstractMapleMapObject {
public boolean isAlive() {
return alive;
}
public boolean isActive() {
return alive && stats.getType(state) != -1;
}
@@ -172,10 +177,12 @@ public class MapleReactor extends AbstractMapleMapObject {
cancelReactorTimeout();
setShouldCollect(true);
refreshReactorTimeout();
if(map != null) map.searchItemReactors(this);
if (map != null) {
map.searchItemReactors(this);
}
}
public void forceHitReactor(final byte newState) {
this.lockReactor();
try {
@@ -185,10 +192,12 @@ public class MapleReactor extends AbstractMapleMapObject {
this.unlockReactor();
}
}
private void tryForceHitReactor(final byte newState) { // weak hit state signal, if already changed reactor state before timeout then drop this
if(!this.reactorLock.tryLock()) return;
if (!this.reactorLock.tryLock()) {
return;
}
try {
this.resetReactorActions(newState);
map.broadcastMessage(MaplePacketCreator.triggerReactor(this, (short) 0));
@@ -196,19 +205,19 @@ public class MapleReactor extends AbstractMapleMapObject {
this.reactorLock.unlock();
}
}
public void cancelReactorTimeout() {
if (timeoutTask != null) {
timeoutTask.cancel(false);
timeoutTask = null;
}
}
private void refreshReactorTimeout() {
int timeOut = stats.getTimeout(state);
if(timeOut > -1) {
if (timeOut > -1) {
final byte nextState = stats.getTimeoutState(state);
timeoutTask = TimerManager.getInstance().schedule(new Runnable() {
@Override
public void run() {
@@ -218,7 +227,7 @@ public class MapleReactor extends AbstractMapleMapObject {
}, timeOut);
}
}
public void delayedHitReactor(final MapleClient c, long delay) {
TimerManager.getInstance().schedule(new Runnable() {
@Override
@@ -231,20 +240,22 @@ public class MapleReactor extends AbstractMapleMapObject {
public void hitReactor(MapleClient c) {
hitReactor(false, 0, (short) 0, 0, c);
}
public void hitReactor(boolean wHit, int charPos, short stance, int skillid, MapleClient c) {
try {
if(!this.isActive()) {
if (!this.isActive()) {
return;
}
if(hitLock.tryLock()) {
if (hitLock.tryLock()) {
this.lockReactor();
try {
cancelReactorTimeout();
attackHit = wHit;
if(ServerConstants.USE_DEBUG == true) c.getPlayer().dropMessage(5, "Hitted REACTOR " + this.getId() + " with POS " + charPos + " , STANCE " + stance + " , SkillID " + skillid + " , STATE " + stats.getType(state) + " STATESIZE " + stats.getStateSize(state));
if (ServerConstants.USE_DEBUG == true) {
c.getPlayer().dropMessage(5, "Hitted REACTOR " + this.getId() + " with POS " + charPos + " , STANCE " + stance + " , SkillID " + skillid + " , STATE " + stats.getType(state) + " STATESIZE " + stats.getStateSize(state));
}
ReactorScriptManager.getInstance().onHit(c, this);
int reactorType = stats.getType(state);
@@ -253,7 +264,9 @@ public class MapleReactor extends AbstractMapleMapObject {
for (byte b = 0; b < stats.getStateSize(state); b++) {//YAY?
List<Integer> activeSkills = stats.getActiveSkills(state, b);
if (activeSkills != null) {
if (!activeSkills.contains(skillid)) continue;
if (!activeSkills.contains(skillid)) {
continue;
}
}
state = stats.getNextState(state, b);
if (stats.getNextState(state, b) == -1) {//end of reactor
@@ -276,7 +289,7 @@ public class MapleReactor extends AbstractMapleMapObject {
setShouldCollect(true); // refresh collectability on item drop-based reactors
refreshReactorTimeout();
if(stats.getType(state) == 100) {
if (stats.getType(state) == 100) {
map.searchItemReactors(this);
}
}
@@ -286,21 +299,23 @@ public class MapleReactor extends AbstractMapleMapObject {
} else {
state++;
map.broadcastMessage(MaplePacketCreator.triggerReactor(this, stance));
ReactorScriptManager.getInstance().act(c, this);
if (this.getId() != 9980000 && this.getId() != 9980001) {
ReactorScriptManager.getInstance().act(c, this);
}
setShouldCollect(true);
refreshReactorTimeout();
if(stats.getType(state) == 100) {
if (stats.getType(state) == 100) {
map.searchItemReactors(this);
}
}
} finally {
this.unlockReactor();
}
hitLock.unlock();
}
} catch(Exception e) {
} catch (Exception e) {
e.printStackTrace();
}
}
@@ -316,4 +331,20 @@ public class MapleReactor extends AbstractMapleMapObject {
public void setName(String name) {
this.name = name;
}
public GuardianSpawnPoint getGuardian() {
return guardian;
}
public void setGuardian(GuardianSpawnPoint guardian) {
this.guardian = guardian;
}
public final void setFacingDirection(final byte facingDirection) {
this.facingDirection = facingDirection;
}
public final byte getFacingDirection() {
return facingDirection;
}
}
+55
View File
@@ -38,6 +38,61 @@ public class MapleReactorFactory {
private static MapleDataProvider data = MapleDataProviderFactory.getDataProvider(new File(System.getProperty("wzpath") + "/Reactor.wz"));
private static Map<Integer, MapleReactorStats> reactorStats = new HashMap<Integer, MapleReactorStats>();
public static final MapleReactorStats getReactorS(int rid) {
MapleReactorStats stats = reactorStats.get(Integer.valueOf(rid));
if (stats == null) {
int infoId = rid;
MapleData reactorData = data.getData(StringUtil.getLeftPaddedStr(Integer.toString(infoId) + ".img", '0', 11));
MapleData link = reactorData.getChildByPath("info/link");
if (link != null) {
infoId = MapleDataTool.getIntConvert("info/link", reactorData);
stats = reactorStats.get(Integer.valueOf(infoId));
}
if (stats == null) {
stats = new MapleReactorStats();
reactorData = data.getData(StringUtil.getLeftPaddedStr(Integer.toString(infoId) + ".img", '0', 11));
if (reactorData == null) {
return stats;
}
boolean canTouch = MapleDataTool.getInt("info/activateByTouch", reactorData, 0) > 0;
boolean areaSet = false;
boolean foundState = false;
for (byte i = 0; true; i++) {
MapleData reactorD = reactorData.getChildByPath(String.valueOf(i));
if (reactorD == null) {
break;
}
MapleData reactorInfoData_ = reactorD.getChildByPath("event");
if (reactorInfoData_ != null && reactorInfoData_.getChildByPath("0") != null) {
MapleData reactorInfoData = reactorInfoData_.getChildByPath("0");
Pair<Integer, Integer> reactItem = null;
int type = MapleDataTool.getIntConvert("type", reactorInfoData);
if (type == 100) { //reactor waits for item
reactItem = new Pair<Integer, Integer>(MapleDataTool.getIntConvert("0", reactorInfoData), MapleDataTool.getIntConvert("1", reactorInfoData, 1));
if (!areaSet) { //only set area of effect for item-triggered reactors once
stats.setTL(MapleDataTool.getPoint("lt", reactorInfoData));
stats.setBR(MapleDataTool.getPoint("rb", reactorInfoData));
areaSet = true;
}
}
foundState = true;
stats.addState(i, type, reactItem, (byte) MapleDataTool.getIntConvert("state", reactorInfoData), MapleDataTool.getIntConvert("timeOut", reactorInfoData_, -1), (byte) (canTouch ? 2 : (MapleDataTool.getIntConvert("2", reactorInfoData, 0) > 0 || reactorInfoData.getChildByPath("clickArea") != null || type == 9 ? 1 : 0)));
} else {
stats.addState(i, 999, null, (byte) (foundState ? -1 : (i + 1)), 0, (byte) 0);
}
}
reactorStats.put(Integer.valueOf(infoId), stats);
if (rid != infoId) {
reactorStats.put(Integer.valueOf(rid), stats);
}
} else { // stats exist at infoId but not rid; add to map
reactorStats.put(Integer.valueOf(rid), stats);
}
}
return stats;
}
public static MapleReactorStats getReactor(int rid) {
MapleReactorStats stats = reactorStats.get(Integer.valueOf(rid));
if (stats == null) {
+7
View File
@@ -22,6 +22,7 @@
package server.maps;
import java.awt.Point;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -58,6 +59,12 @@ public class MapleReactorStats {
if(timeOut > -1) timeoutInfo.put(state, timeOut);
}
public void addState(byte state, int type, Pair<Integer, Integer> reactItem, byte nextState, int timeOut, byte canTouch) {
List<StateData> data = new ArrayList<>();
data.add(new StateData(type, reactItem, null, nextState));
stateInfo.put(state, data);
}
public int getTimeout(byte state) {
Integer i = timeoutInfo.get(state);
return (i == null) ? -1 : i;
+1
View File
@@ -31,6 +31,7 @@ public enum SavedLocationType {
EVENT,
BOSSPQ,
HAPPYVILLE,
MONSTER_CARNIVAL,
DEVELOPER;
public static SavedLocationType fromString(String Str) {