/** ********************************************************************************** * 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.data.swgfile.ClientFactory; 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; } return new NPCTable(tableChance, lootTable); } 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; } lootAll(cci.getSource(), cci.getTarget()); } 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; } int itemGroupRoll = random.nextInt(100) + 1; int minInterval = 1; for (LootGroup itemGroup : lootTable.getLootGroups()) { 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_")) { // TODO dynamic item handling new ChatBroadcastIntent(requester.getOwner(), "We don't support this loot item yet: " + randomItemName).broadcast(); } else if (randomItemName.endsWith(".iff")) { String sharedTemplate = ClientFactory.formatToSharedFile(randomItemName); SWGObject object = ObjectCreator.createObjectFromTemplate(sharedTemplate); object.moveToContainer(lootInventory); new ObjectCreatedIntent(object).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; } } }