mirror of
https://github.com/ProjectSWGCore/Holocore.git
synced 2026-10-19 09:03:02 -04:00
466 lines
15 KiB
Java
466 lines
15 KiB
Java
/** **********************************************************************************
|
|
* 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 <http://www.gnu.org/licenses/>. * *
|
|
**********************************************************************************
|
|
*/
|
|
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<String, LootTable> lootTables; // K: loot_id, V: table contents
|
|
private final Map<String, NPCLoot> 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<RadialOption> options = new ArrayList<RadialOption>(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<SWGObject> 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<NPCTable> 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<NPCTable> 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<LootGroup> lootGroups;
|
|
|
|
public LootTable() {
|
|
lootGroups = new ArrayList<>();
|
|
}
|
|
|
|
public void addLootGroup(LootGroup lootGroup) {
|
|
lootGroups.add(lootGroup);
|
|
}
|
|
|
|
public Collection<LootGroup> 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;
|
|
}
|
|
}
|
|
|
|
}
|