diff --git a/src/resources/objects/custom/AIObject.java b/src/resources/objects/custom/AIObject.java
index 35a5770ba..2160973d9 100644
--- a/src/resources/objects/custom/AIObject.java
+++ b/src/resources/objects/custom/AIObject.java
@@ -39,6 +39,7 @@ public abstract class AIObject extends CreatureObject {
private long initialDelay;
private long delay;
private TimeUnit unit;
+ private String creatureId;
public AIObject(long objectId) {
super(objectId);
@@ -92,5 +93,12 @@ public abstract class AIObject extends CreatureObject {
future.cancel(true);
future = null;
}
-
+
+ public String getCreatureId() {
+ return creatureId;
+ }
+
+ public void setCreatureId(String creatureId) {
+ this.creatureId = creatureId;
+ }
}
diff --git a/src/resources/spawn/Spawner.java b/src/resources/spawn/Spawner.java
index c64ea51e5..b4c47ce43 100644
--- a/src/resources/spawn/Spawner.java
+++ b/src/resources/spawn/Spawner.java
@@ -36,10 +36,11 @@ import resources.objects.creature.CreatureDifficulty;
import resources.objects.custom.AIBehavior;
public final class Spawner {
-
+
private final Random random;
private final Location location;
private final int id;
+ private String creatureId;
private SWGObject eggObject;
private String creatureName;
private String[] iffTemplates;
@@ -59,7 +60,15 @@ public final class Spawner {
this.random = new Random();
this.location = new Location();
}
-
+
+ public void setCreatureId(String creatureId) {
+ this.creatureId = creatureId;
+ }
+
+ public String getCreatureId() {
+ return creatureId;
+ }
+
public int getSpawnerId() {
return id;
}
diff --git a/src/services/combat/CombatManager.java b/src/services/combat/CombatManager.java
index 95e9dac57..37b65b0db 100644
--- a/src/services/combat/CombatManager.java
+++ b/src/services/combat/CombatManager.java
@@ -91,6 +91,7 @@ public class CombatManager extends Manager {
private final CorpseService corpseService;
private final CombatXpService combatXpService;
private final DuelPlayerService duelPlayerService;
+ private final LootService lootService;
private ScheduledExecutorService executor;
@@ -109,9 +110,12 @@ public class CombatManager extends Manager {
corpseService = new CorpseService();
combatXpService = new CombatXpService();
duelPlayerService = new DuelPlayerService();
+ lootService = new LootService();
+
addChildService(corpseService);
addChildService(combatXpService);
addChildService(duelPlayerService);
+ addChildService(lootService);
registerForIntent(DeathblowIntent.class, di -> handleDeathblowIntent(di));
registerForIntent(ChatCommandIntent.class, cci -> handleChatCommandIntent(cci));
@@ -326,7 +330,7 @@ public class CombatManager extends Manager {
// Show particle effect to everyone observing the delay egg, if one is defined
if (delayEgg != null && !delayAttackParticle.isEmpty())
- delayEgg.sendObservers(new PlayClientEffectObjectMessage(delayAttackParticle, "", delayEgg.getObjectId()));
+ delayEgg.sendObservers(new PlayClientEffectObjectMessage(delayAttackParticle, "", delayEgg.getObjectId(), ""));
// Handle the attack of this loop
handleAttack(source, target, delayEgg, combatCommand);
diff --git a/src/services/combat/CorpseService.java b/src/services/combat/CorpseService.java
index b1339f7f5..8669907ca 100644
--- a/src/services/combat/CorpseService.java
+++ b/src/services/combat/CorpseService.java
@@ -287,7 +287,7 @@ public final class CorpseService extends Service {
Log.e("Cannot delete the corpse of a player!", creatureCorpse);
} else {
new DestroyObjectIntent(creatureCorpse).broadcast();
- Log.i("Corpse of NPC %s was deleted from the world", creatureCorpse);
+ Log.d("Corpse of NPC %s was deleted from the world", creatureCorpse);
}
}
@@ -379,7 +379,7 @@ public final class CorpseService extends Service {
corpse.setTurnScale(1);
corpse.setMovementScale(1);
corpse.setHealth(corpse.getMaxHealth());
- corpse.sendObserversAndSelf(new PlayClientEffectObjectMessage("clienteffect/player_clone_compile.cef", "", corpse.getObjectId()));
+ corpse.sendObserversAndSelf(new PlayClientEffectObjectMessage("clienteffect/player_clone_compile.cef", "", corpse.getObjectId(), ""));
BuffIntent cloningSickness = new BuffIntent("cloning_sickness", corpse, corpse, false);
new BuffIntent("incapWeaken", corpse, corpse, true).broadcastAfterIntent(cloningSickness);
diff --git a/src/services/combat/LootService.java b/src/services/combat/LootService.java
new file mode 100644
index 000000000..b5ac814cf
--- /dev/null
+++ b/src/services/combat/LootService.java
@@ -0,0 +1,461 @@
+/** **********************************************************************************
+ * Copyright (c) 2015 /// Project SWG /// www.projectswg.com *
+ * *
+ * ProjectSWG is the first NGE emulator for Star Wars Galaxies founded on *
+ * July 7th, 2011 after SOE announced the official shutdown of Star Wars Galaxies. *
+ * Our goal is to create an emulator which will provide a server for players to *
+ * continue playing a game similar to the one they used to play. We are basing *
+ * it on the final publish of the game prior to end-game events. *
+ * *
+ * This file is part of Holocore. *
+ * *
+ * -------------------------------------------------------------------------------- *
+ * *
+ * Holocore 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, either version 3 of the *
+ * License, or (at your option) any later version. *
+ * *
+ * Holocore 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 Holocore. If not, see . * *
+ **********************************************************************************
+ */
+package services.combat;
+
+import com.projectswg.common.control.Service;
+import com.projectswg.common.data.info.RelationalDatabase;
+import com.projectswg.common.data.info.RelationalServerFactory;
+import com.projectswg.common.data.location.Location;
+import com.projectswg.common.debug.Log;
+import intents.chat.ChatBroadcastIntent;
+import intents.chat.ChatCommandIntent;
+import intents.combat.CreatureKilledIntent;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.*;
+
+import intents.object.CreateStaticItemIntent;
+import intents.object.ObjectCreatedIntent;
+import intents.radial.RadialRequestIntent;
+import intents.radial.RadialResponseIntent;
+import intents.radial.RadialSelectionIntent;
+import network.packets.swg.zone.ClientOpenContainerMessage;
+import network.packets.swg.zone.PlayClientEffectObjectTransformMessage;
+import resources.objects.SWGObject;
+import resources.objects.creature.CreatureDifficulty;
+import resources.objects.creature.CreatureObject;
+import resources.objects.custom.AIObject;
+import resources.objects.tangible.TangibleObject;
+import resources.radial.RadialItem;
+import resources.radial.RadialOption;
+import resources.server_info.StandardLog;
+import services.objects.ObjectCreator;
+import services.objects.StaticItemService;
+
+public final class LootService extends Service {
+
+ private static final String LOOT_TABLE_SELECTOR = "SELECT * FROM loot_table";
+ private static final String NPC_LOOT_SELECTOR = "SELECT npc_id, min_cash, max_cash, loot_table1_chance, loot_table1, loot_table2_chance, loot_table2, loot_table3_chance, loot_table3 FROM npc";
+
+ private final Map lootTables; // K: loot_id, V: table contents
+ private final Map npcLoot; // K: npc_id, V: possible loot
+ private final Random random;
+
+ public LootService() {
+ lootTables = new HashMap<>();
+ npcLoot = new HashMap<>();
+ random = new Random();
+
+ registerForIntent(ChatCommandIntent.class, cci -> handleChatCommand(cci));
+ registerForIntent(CreatureKilledIntent.class, cki -> handleCreatureKilled(cki));
+ registerForIntent(RadialSelectionIntent.class, rsi -> handleRadialSelection(rsi));
+ registerForIntent(RadialRequestIntent.class, rri -> handleRadialRequestIntent(rri));
+ }
+
+ @Override
+ public boolean initialize() {
+ loadLootTables();
+ loadNPCLoot();
+
+ return super.initialize();
+ }
+
+ private void loadLootTables() {
+ String what = "loot tables";
+ long startTime = StandardLog.onStartLoad(what);
+
+ try (RelationalDatabase spawnerDatabase = RelationalServerFactory.getServerData("loot/loot_table.db", "loot_table")) {
+ try (ResultSet set = spawnerDatabase.executeQuery(LOOT_TABLE_SELECTOR)) {
+ while (set.next()) {
+ loadLootTable(set);
+ }
+ } catch (SQLException e) {
+ Log.e(e);
+ }
+ }
+
+ StandardLog.onEndLoad(lootTables.size(), what, startTime);
+ }
+
+ private void loadLootTable(ResultSet set) throws SQLException {
+ String tableName = set.getString("loot_id");
+
+ if (tableName.equals("-")) {
+ return;
+ }
+
+ LootTable table = new LootTable();
+ byte totalChance = 0; // Must not be above 100
+
+ for (int groupNum = 1; groupNum <= 16 && totalChance <= 100; groupNum++) {
+ LootGroup lootGroup = loadLootGroup(set, groupNum);
+
+ table.addLootGroup(lootGroup);
+ totalChance += lootGroup.getChance();
+ }
+
+ lootTables.put(tableName, table);
+ }
+
+ private LootGroup loadLootGroup(ResultSet set, int groupNum) throws SQLException {
+ String groupItems = set.getString("items_group_" + groupNum);
+ int groupChance = set.getInt("chance_group_" + groupNum);
+ String[] itemNames = groupItems.split(";");
+
+ return new LootGroup(groupChance, itemNames);
+ }
+
+ private void loadNPCLoot() {
+ String what = "NPC loot links";
+ long startTime = StandardLog.onStartLoad(what);
+
+ try (RelationalDatabase spawnerDatabase = RelationalServerFactory.getServerData("npc/npc.db", "npc")) {
+ try (ResultSet set = spawnerDatabase.executeQuery(NPC_LOOT_SELECTOR)) {
+ while (set.next()) {
+ loadNPCLoot(set);
+ }
+ } catch (SQLException e) {
+ Log.e(e);
+ }
+ }
+
+ StandardLog.onEndLoad(npcLoot.size(), what, startTime);
+ }
+
+ private void loadNPCLoot(ResultSet set) throws SQLException {
+ int minCash = set.getInt("min_cash");
+ int maxCash = set.getInt("max_cash");
+ String creatureId = set.getString("npc_id");
+ NPCLoot loot = new NPCLoot(minCash, maxCash);
+
+ for (byte tableNum = 1; tableNum <= 3; tableNum++) {
+ NPCTable npcTable = loadNPCTable(set, tableNum);
+
+ if (npcTable == null || npcTable.getChance() <= 0) {
+ continue;
+ }
+
+ loot.addNPCTable(npcTable);
+ }
+
+ npcLoot.put(creatureId, loot);
+ }
+
+ private NPCTable loadNPCTable(ResultSet set, int tableNum) throws SQLException {
+ String columnName = "loot_table" + tableNum;
+ String columnChance = columnName + "_chance";
+ String tableName = set.getString(columnName);
+ int tableChance = set.getInt(columnChance);
+ LootTable lootTable = lootTables.get(tableName);
+
+ if (lootTable == null) {
+ return null;
+ }
+
+ NPCTable npcTable = new NPCTable(tableChance, lootTable);
+
+ return npcTable;
+ }
+
+ private void handleCreatureKilled(CreatureKilledIntent cki) {
+ CreatureObject corpse = cki.getCorpse();
+
+ if (corpse.isPlayer()) {
+ // Players don't drop loot
+ return;
+ }
+
+ String creatureId = ((AIObject) corpse).getCreatureId();
+ NPCLoot loot = npcLoot.get(creatureId);
+
+ if (loot == null) {
+ Log.w("No NPCLoot associated with NPC ID: " + creatureId);
+ return;
+ }
+
+ SWGObject lootInventory = ObjectCreator.createObjectFromTemplate("object/tangible/inventory/shared_creature_inventory.iff");
+ lootInventory.setLocation(corpse.getLocation());
+ // TODO set permissions on lootInventory
+ corpse.addObject(lootInventory); // It's a slotted object and goes in the inventory slot
+ new ObjectCreatedIntent(lootInventory).broadcast();
+
+ CreatureObject killer = cki.getKiller();
+
+ // generateCreditChip(loot, killer, lootInventory, corpse.getDifficulty());
+ generateLoot(loot, killer, lootInventory);
+ }
+
+ private void handleChatCommand(ChatCommandIntent cci) {
+ if(!cci.getCommand().getName().equalsIgnoreCase("loot")) {
+ return;
+ }
+
+ SWGObject target = cci.getTarget();
+
+ lootAll(cci.getSource(), target);
+ }
+
+ private void handleRadialSelection(RadialSelectionIntent rsi) {
+ switch (rsi.getSelection()) {
+ case LOOT:
+ // TODO permissions check
+ SWGObject inventory = rsi.getTarget().getSlottedObject("inventory");
+
+ if (inventory.getContainedObjects().isEmpty()) {
+ // Don't bother opening a window for an inventory with no loot in it
+ return;
+ }
+
+ rsi.getPlayer().sendPacket(new ClientOpenContainerMessage(inventory.getObjectId(), ""));
+ break;
+ case LOOT_ALL:
+ lootAll(rsi.getPlayer().getCreatureObject(), rsi.getTarget());
+ break;
+ }
+ }
+
+ private void handleRadialRequestIntent(RadialRequestIntent rri){
+ SWGObject target = rri.getTarget();
+
+ if (!(target instanceof AIObject)) {
+ // We can only loot NPCs
+ return;
+ }
+
+ CreatureObject creature = (CreatureObject) target;
+
+ if (creature.getHealth() > 0) {
+ // Live creatures shouldn't get a loot radial
+ return;
+ }
+
+ // TODO permissions check
+
+ List options = new ArrayList(rri.getRequest().getOptions());
+ RadialOption loot = new RadialOption(RadialItem.LOOT);
+ loot.addChild(RadialItem.LOOT_ALL);
+ options.add(loot);
+ new RadialResponseIntent(rri.getPlayer(), target, options, rri.getRequest().getCounter()).broadcast();
+ }
+
+ private void lootAll(SWGObject looter, SWGObject corpse) {
+ if (!(corpse instanceof AIObject)) {
+ // Can't loot something that's not a NPC
+ return;
+ }
+
+ SWGObject lootInventory = corpse.getSlottedObject("inventory");
+
+ // TODO respect loot rules for groups
+ // TODO permissions check
+
+ Collection loot = lootInventory.getContainedObjects(); // No concurrent modification because a copy Collection is returned
+
+ loot.forEach(item -> item.moveToContainer(looter.getSlottedObject("inventory")));
+ }
+
+ private void showLootDisc(CreatureObject requester, SWGObject corpse) {
+ SWGObject inventory = corpse.getSlottedObject("inventory");
+
+ // At this point, something will have dropped for sure.
+ if (requester.isPlayer() && !inventory.getContainedObjects().isEmpty()) { // TODO needs adjustment for group loot
+ // If there's something we can loot, draw the loot disc icon on the corpse!
+ Location effectLocation = new Location(corpse.getLocation());
+ effectLocation.setPosition(0,0.5, 0); // TODO should 0.5 be hardcoded or grabbed from somewhere?
+
+ // TODO display it to everyone with access to lootInventory. For now, display the disc to the killer
+ requester.getOwner().sendPacket(new PlayClientEffectObjectTransformMessage(corpse.getObjectId(), "appearance/pt_loot_disc.prt", effectLocation, "lootMe"));
+ }
+ }
+
+ private void generateCreditChip(NPCLoot loot, CreatureObject killer, SWGObject inventory, CreatureDifficulty difficulty) {
+ int maxCash = loot.getMaxCash();
+
+ if (maxCash == 0) {
+ // No cash is ever dropped on this creature
+ return;
+ }
+
+ int minCash = loot.getMinCash();
+ int cashAmount = random.nextInt((maxCash - minCash) + 1) + minCash;
+
+ switch (difficulty) {
+ default:
+ case NORMAL: cashAmount *= 1; break;
+ case ELITE: cashAmount *= 2; break;
+ case BOSS: cashAmount *= 3; break;
+ }
+
+ // TODO scale with group size?
+
+ TangibleObject cashObject = ObjectCreator.createObjectFromTemplate("object/tangible/item/shared_loot_cash.iff", TangibleObject.class);
+
+ cashObject.setObjectName(cashAmount + " cr");
+ cashObject.moveToContainer(inventory);
+
+ new ObjectCreatedIntent(cashObject).broadcast();
+
+ showLootDisc(killer, inventory.getParent());
+ }
+
+ private void generateLoot(NPCLoot loot, CreatureObject requester, SWGObject lootInventory) {
+ int tableRoll = random.nextInt(100) + 1;
+
+ for (NPCTable npcTable : loot.getNPCTables()) {
+ LootTable lootTable = npcTable.getLootTable();
+ int tableChance = npcTable.getChance();
+
+ if (tableChance == 0 || tableChance < tableRoll ) {
+ // Skip ahead if there's no drop chance
+ continue;
+ }
+
+ Collection itemGroups = lootTable.getLootGroups();
+ int itemGroupRoll = random.nextInt(100) + 1;
+ int minInterval = 1;
+
+ for (LootGroup itemGroup : itemGroups) {
+ int groupChance = itemGroup.getChance();
+
+ if (minInterval < itemGroupRoll && itemGroupRoll > groupChance) {
+ minInterval += groupChance;
+ // Check next item group
+ continue;
+ }
+
+ String[] itemNames = itemGroup.getItemNames();
+ String randomItemName = itemNames[random.nextInt(itemNames.length)]; // Selects a completely random item from the group
+
+ if (randomItemName.startsWith("dynamic_") || randomItemName.endsWith(".iff")) {
+ // TODO dynamic item handling
+ new ChatBroadcastIntent(requester.getOwner(), "We don't support this loot item yet: " + randomItemName).broadcast();
+ } else {
+ new CreateStaticItemIntent(requester, lootInventory, new StaticItemService.ObjectCreationHandler() {
+ @Override
+ public void success(SWGObject[] createdObjects) {
+ showLootDisc(requester, lootInventory.getParent());
+ }
+
+ @Override
+ public boolean isIgnoreVolume() {
+ return true;
+ }
+ }, randomItemName).broadcast();
+ }
+
+ break; // Only one group is ever spawned
+ }
+ }
+ }
+
+ private static class NPCLoot {
+
+ private final int minCash;
+ private final int maxCash;
+ private final Collection npcTables;
+
+ public NPCLoot(int minCash, int maxCash) {
+ this.minCash = minCash;
+ this.maxCash = maxCash;
+ npcTables = new ArrayList<>();
+ }
+
+ public int getMinCash() {
+ return minCash;
+ }
+
+ public int getMaxCash() {
+ return maxCash;
+ }
+
+ public Collection getNPCTables() {
+ return npcTables;
+ }
+
+ public void addNPCTable(NPCTable npcTable) {
+ npcTables.add(npcTable);
+ }
+
+ }
+
+ private static class NPCTable {
+
+ private final int chance;
+ private final LootTable lootTable;
+
+ public NPCTable(int chance, LootTable lootTable) {
+ this.chance = chance;
+ this.lootTable = lootTable;
+ }
+
+ public int getChance() {
+ return chance;
+ }
+
+ public LootTable getLootTable() {
+ return lootTable;
+ }
+ }
+
+ private static class LootTable {
+
+ private final Collection lootGroups;
+
+ public LootTable() {
+ lootGroups = new ArrayList<>();
+ }
+
+ public void addLootGroup(LootGroup lootGroup) {
+ lootGroups.add(lootGroup);
+ }
+
+ public Collection getLootGroups() {
+ return lootGroups;
+ }
+ }
+
+ private static class LootGroup {
+
+ private final int chance;
+ private final String[] itemNames;
+
+ public LootGroup(int chance, String[] staticItems) {
+ this.chance = chance;
+ this.itemNames = staticItems;
+ }
+
+ public int getChance() {
+ return chance;
+ }
+
+ public String[] getItemNames() {
+ return itemNames;
+ }
+ }
+
+}
diff --git a/src/services/commands/BuffService.java b/src/services/commands/BuffService.java
index 26cb8defe..3b5996b9b 100644
--- a/src/services/commands/BuffService.java
+++ b/src/services/commands/BuffService.java
@@ -310,7 +310,7 @@ public class BuffService extends Service {
private void sendParticleEffect(String effectFileName, CreatureObject receiver, String hardPoint) {
if (!effectFileName.isEmpty()) {
- receiver.sendObserversAndSelf(new PlayClientEffectObjectMessage(effectFileName, hardPoint, receiver.getObjectId()));
+ receiver.sendObserversAndSelf(new PlayClientEffectObjectMessage(effectFileName, hardPoint, receiver.getObjectId(), ""));
}
}
diff --git a/src/services/experience/SkillTemplateService.java b/src/services/experience/SkillTemplateService.java
index ec1347d44..2df4f764e 100644
--- a/src/services/experience/SkillTemplateService.java
+++ b/src/services/experience/SkillTemplateService.java
@@ -155,7 +155,7 @@ public final class SkillTemplateService extends Service {
flyTextColor = new RGB(Color.BLUE);
}
- creatureObject.sendObserversAndSelf(new PlayClientEffectObjectMessage(effectFile, "", objectId));
+ creatureObject.sendObserversAndSelf(new PlayClientEffectObjectMessage(effectFile, "", objectId, ""));
player.sendPacket(new ShowFlyText(objectId, new StringId("cbt_spam", flyText), Scale.LARGEST, flyTextColor));
if (skillUp)
diff --git a/src/services/objects/StaticItemService.java b/src/services/objects/StaticItemService.java
index 2920a8602..919d63610 100644
--- a/src/services/objects/StaticItemService.java
+++ b/src/services/objects/StaticItemService.java
@@ -194,7 +194,7 @@ public final class StaticItemService extends Service {
switch(object.moveToContainer(container)) { // Server-generated object is added to the container
case SUCCESS:
- Log.i("Successfully moved %s into container %s", itemName, container);
+ Log.d("Successfully moved %s into container %s", itemName, container);
createdObjects[j] = object;
break;
default:
diff --git a/src/services/spawn/SpawnerService.java b/src/services/spawn/SpawnerService.java
index 7bfa9823b..a7ba11baa 100644
--- a/src/services/spawn/SpawnerService.java
+++ b/src/services/spawn/SpawnerService.java
@@ -72,7 +72,7 @@ public final class SpawnerService extends Service {
+ "static.spawner_type, static.cell_id, static.active, static.mood, static.behaviour, static.float_radius, " // more static columns
+ "static.min_spawn_time, static.max_spawn_time, static.amount, static.spawn_id, " // even more static columns
+ "buildings.object_id AS building_id, buildings.terrain_name AS building_terrain, " // building columns
- + "npc.iff_template AS iff, npc.npc_name, npc.combat_level, npc.difficulty, npc.attackable, " // npc columns
+ + "npc.npc_id, npc.iff_template AS iff, npc.npc_name, npc.combat_level, npc.difficulty, npc.attackable, " // npc columns
+ "npc_stats.HP, npc_stats.Action, npc_stats.Boss_HP, npc_stats.Boss_Action, npc_stats.Elite_HP, npc_stats.Elite_Action " // npc_stats columns
+ "FROM static, buildings, npc, npc_stats "
+ "WHERE buildings.building_id = static.building_id AND static.npc_id = npc.npc_id AND npc.combat_level = npc_stats.Level";
@@ -161,6 +161,7 @@ public final class SpawnerService extends Service {
private void loadSpawner(ResultSet set, Location loc) throws SQLException {
Spawner spawner = new Spawner(set.getInt("spawn_id"));
+ spawner.setCreatureId(set.getString("npc_id"));
spawner.setIffTemplates(createTemplateList(set.getString("iff")));
spawner.setCreatureName(set.getString("npc_name").intern());
spawner.setCombatLevel(set.getShort("combat_level"));
@@ -298,6 +299,7 @@ public final class SpawnerService extends Service {
object.setMoodAnimation(spawner.getMoodAnimation());
object.setBehavior(spawner.getAIBehavior());
object.setFloatRadius(spawner.getFloatRadius());
+ object.setCreatureId(spawner.getCreatureId());
setFlags(object, spawner.getSpawnerFlag());
object.moveToContainer(spawner.getSpawnerObject().getParent());