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());