Converted databases to null-safe delegates and converted LootGenerationService to kotlin

This commit is contained in:
Obique
2019-05-19 12:35:23 -05:00
parent 2174fe406d
commit c60dedfa99
10 changed files with 403 additions and 459 deletions
@@ -68,7 +68,7 @@ public class SdbLoader {
return ext.substring(lastPeriod+1);
}
public interface SdbResultSet extends AutoCloseable {
public interface SdbResultSet extends Closeable, AutoCloseable {
@Override
void close() throws IOException;
@@ -92,6 +92,10 @@ public abstract class DataLoader {
return getInstance(BuildingCellLoader.class, BuildingCellLoader::new);
}
public static LootTableLoader lootTables() {
return getInstance(LootTableLoader.class, LootTableLoader::new);
}
public static NpcLoader npcs() {
return getInstanceWeakCaching(NpcLoader.class, NpcLoader::new);
}
@@ -0,0 +1,96 @@
/***********************************************************************************
* Copyright (c) 2019 /// 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 com.projectswg.holocore.resources.support.data.server_info.loader.npc
import com.projectswg.holocore.resources.support.data.server_info.SdbColumnArraySet
import com.projectswg.holocore.resources.support.data.server_info.SdbLoader
import com.projectswg.holocore.resources.support.data.server_info.SdbLoader.SdbResultSet
import com.projectswg.holocore.resources.support.data.server_info.loader.DataLoader
import java.io.File
import java.io.IOException
import java.util.*
import java.util.stream.Collectors.groupingBy
import kotlin.math.min
class LootTableLoader : DataLoader() {
private var lootTables: Map<String, Map<String, List<LootTableItem>>> = HashMap()
fun getLootTableItem(table: String, difficulty: String, level: Int): LootTableItem? {
return lootTables[table]?.get(difficulty)?.first { it.minLevel <= level && it.maxLevel > level }
}
@Throws(IOException::class)
public override fun load() {
SdbLoader.load(File("serverdata/loot/loot_table.sdb")).use { set ->
val itemGroups = set.getTextArrayParser("items_group_([0-9]+)")
val chanceGroups = set.getIntegerArrayParser("chance_group_([0-9]+)")
lootTables = set
.stream { LootTableItem(it, itemGroups, chanceGroups) }
.collect(groupingBy(LootTableItem::id))
.mapValues { it.value.groupBy(LootTableItem::difficulty) }
}
}
class LootTableItem(set: SdbResultSet, itemGroups: SdbColumnArraySet.SdbTextColumnArraySet, chanceGroups: SdbColumnArraySet.SdbIntegerColumnArraySet) {
val id = set.getText("loot_id")!!
val difficulty = set.getText("difficulty")!!
val minLevel = set.getInt("min_cl").toInt()
val maxLevel = set.getInt("max_cl").toInt()
val groups: List<LootGroup>
init {
val groups = itemGroups.getArray(set)
val chances = chanceGroups.getArray(set)
val count = min(groups.size, chances.size)
val groupList = ArrayList<LootGroup>()
var chance = 0
for (i in 0 until count) {
val group = groups[i] ?: continue
val itemChance = chances[i]
if (itemChance == 0 || itemChance == Integer.MAX_VALUE)
continue
chance += itemChance
groupList.add(LootGroup(chance, group.split(";").filter { it.isNotBlank() }))
}
this.groups = groupList
}
override fun toString(): String {
return "LootTableItem(id=$id, difficulty=$difficulty, level=[$minLevel, $maxLevel] groups=$groups)"
}
}
data class LootGroup(val chance: Int, val items: List<String>)
}
@@ -40,11 +40,7 @@ import java.util.stream.Collectors.groupingBy
class NpcCombatProfileLoader : DataLoader() {
private val profiles: MutableMap<String, List<CombatProfile>>
init {
this.profiles = HashMap()
}
private val profiles: MutableMap<String, List<CombatProfile>> = HashMap()
fun getAbilities(id: String?, difficulty: CreatureDifficulty, level: Int): Set<String> {
val subprofiles = Objects.requireNonNull<List<CombatProfile>>(profiles[id], "unknown profile id")
@@ -58,24 +54,16 @@ class NpcCombatProfileLoader : DataLoader() {
@Throws(IOException::class)
public override fun load() {
SdbLoader.load(File("serverdata/spawn/static.msdb")).use { set -> profiles.putAll(set.stream<CombatProfile>(Function<SdbResultSet, CombatProfile> { CombatProfile(it) }).collect<Map<String, List<CombatProfile>>, Any>(groupingBy(Function<CombatProfile, String> { it.getId() }))) }
SdbLoader.load(File("serverdata/spawn/static.msdb")).use { set -> profiles.putAll(set.stream { CombatProfile(it) }.collect(groupingBy { it.id })) }
}
private class CombatProfile(set: SdbResultSet) {
val id: String
val difficulty: CreatureDifficulty
val minLevel: Int
val maxLevel: Int
val abilities: Set<String>
init {
this.id = set.getText("profile_id")
this.difficulty = CreatureDifficulty.valueOf(set.getText("difficulty"))
this.minLevel = set.getInt("min_cl").toInt()
this.maxLevel = set.getInt("max_cl").toInt()
this.abilities = Set.of<String>(*set.getText("action").split(";".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray())
}
val id = set.getText("profile_id")!!
val difficulty = CreatureDifficulty.valueOf(set.getText("difficulty"))
val minLevel = set.getInt("min_cl").toInt()
val maxLevel = set.getInt("max_cl").toInt()
val abilities = setOf(*set.getText("action").split(";".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray())
}
@@ -2,37 +2,30 @@ package com.projectswg.holocore.resources.support.data.server_info.mongodb
import com.mongodb.WriteConcern
import com.mongodb.client.MongoClients
import com.mongodb.client.MongoCollection
import com.mongodb.client.MongoDatabase
import me.joshlarson.jlcommon.log.Log
import org.jetbrains.annotations.NotNull
import org.bson.Document
import java.util.logging.Handler
import java.util.logging.Level
import java.util.logging.LogRecord
import java.util.logging.Logger
import kotlin.reflect.KProperty
object PswgDatabase {
var config: PswgConfigDatabase? = null
@NotNull get() = field ?: PswgConfigDatabase(null)
private set
var users: PswgUserDatabase? = null
@NotNull get() = field!!
private set
var objects: PswgObjectDatabase? = null
@NotNull get() = field!!
private set
var resources: PswgResourceDatabase? = null
@NotNull get() = field!!
private set
private val database = ObjectRef<MongoDatabase?>(null)
val config by DatabaseDelegate(database, "config") { PswgConfigDatabase(it?.withWriteConcern(WriteConcern.ACKNOWLEDGED)) }
val users by DatabaseDelegate(database, "users") { PswgUserDatabase(it?.withWriteConcern(WriteConcern.ACKNOWLEDGED)) }
val objects by DatabaseDelegate(database, "objects") { PswgObjectDatabase(it?.withWriteConcern(WriteConcern.JOURNALED)) }
val resources by DatabaseDelegate(database, "resources") { PswgResourceDatabase(it?.withWriteConcern(WriteConcern.ACKNOWLEDGED)) }
fun initialize(connectionString: String, databaseName: String) {
setupMongoLogging()
val client = MongoClients.create(connectionString)
val database = client.getDatabase(databaseName)
config = PswgConfigDatabase(database.getCollection("config").withWriteConcern(WriteConcern.ACKNOWLEDGED))
users = PswgUserDatabase(database.getCollection("users").withWriteConcern(WriteConcern.ACKNOWLEDGED))
objects = PswgObjectDatabase(database.getCollection("objects").withWriteConcern(WriteConcern.JOURNALED))
resources = PswgResourceDatabase(database.getCollection("resources").withWriteConcern(WriteConcern.ACKNOWLEDGED))
this.database.element = database
}
private fun setupMongoLogging() {
@@ -61,4 +54,22 @@ object PswgDatabase {
})
}
private class DatabaseDelegate<T>(private val database: ObjectRef<MongoDatabase?>, private val collectionName: String, private var databaseSupplier: (MongoCollection<Document>?) -> T) {
private var prevDatabase: MongoDatabase? = null
private var prevReturn: T = databaseSupplier(null)
operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
val database = this.database.element
if (prevDatabase != database) {
this.prevDatabase = database
this.prevReturn = databaseSupplier(database?.getCollection(collectionName))
}
return prevReturn
}
}
private class ObjectRef<T: Any?>(var element: T)
}
@@ -37,20 +37,22 @@ import java.util.*
import java.util.regex.Pattern
import java.util.stream.Collectors.toList
class PswgObjectDatabase(private val collection: MongoCollection<Document>) {
class PswgObjectDatabase(private val collection: MongoCollection<Document>?) {
val objects: List<MongoData>
get() = collection.find().map { MongoData(it) }.into(ArrayList())
get() = collection?.find()?.map { MongoData(it) }?.into(ArrayList()) ?: ArrayList()
init {
collection.createIndex(Indexes.ascending("id"), IndexOptions().unique(true))
collection?.createIndex(Indexes.ascending("id"), IndexOptions().unique(true))
}
fun addObject(obj: SWGObject) {
collection ?: return
collection.replaceOne(Filters.eq("id", obj.objectId), SWGObjectFactory.save(obj, MongoData()).toDocument(), ReplaceOptions().upsert(true))
}
fun addObjects(objects: Collection<SWGObject>) {
collection ?: return
if (objects.isEmpty())
return
collection.bulkWrite(objects.stream()
@@ -66,14 +68,17 @@ class PswgObjectDatabase(private val collection: MongoCollection<Document>) {
}
fun removeObject(id: Long): Boolean {
collection ?: return true
return collection.deleteOne(Filters.eq("id", id)).deletedCount > 0
}
fun getCharacterCount(account: String): Int {
collection ?: return 0
return collection.countDocuments(Filters.eq("account", account)).toInt()
}
fun isCharacter(firstName: String): Boolean {
collection ?: return false
return collection.countDocuments(Filters.and(
Filters.regex("template", "object/creature/player/shared_.+\\.iff"),
Filters.regex("base3.objectName", Pattern.compile(Pattern.quote(firstName) + "( .+|$)", Pattern.CASE_INSENSITIVE))
@@ -81,6 +86,7 @@ class PswgObjectDatabase(private val collection: MongoCollection<Document>) {
}
fun clearObjects(): Long {
collection ?: return 0
return collection.deleteMany(Filters.exists("_id")).deletedCount
}
@@ -35,12 +35,12 @@ import org.bson.Document
import java.util.*
import java.util.stream.Collectors.toList
class PswgResourceDatabase(private val collection: MongoCollection<Document>) {
class PswgResourceDatabase(private val collection: MongoCollection<Document>?) {
var resources: List<GalacticResource>
get() = collection.find().map { MongoData.create(it) { GalacticResource() } }.into(ArrayList())
get() = collection?.find()?.map { MongoData.create(it) { GalacticResource() } }?.into(ArrayList()) ?: ArrayList()
set(value) {
collection.bulkWrite(value.stream()
collection?.bulkWrite(value.stream()
.map { resource ->
ReplaceOneModel(
Filters.eq("id", resource.id), // match the resource id
@@ -53,7 +53,7 @@ class PswgResourceDatabase(private val collection: MongoCollection<Document>) {
}
init {
collection.createIndex(Indexes.ascending("id"), IndexOptions().unique(true))
collection?.createIndex(Indexes.ascending("id"), IndexOptions().unique(true))
}
}
@@ -33,17 +33,17 @@ import com.mongodb.client.model.IndexOptions
import com.mongodb.client.model.Indexes
import org.bson.Document
class PswgUserDatabase(private val collection: MongoCollection<Document>) {
class PswgUserDatabase(private val collection: MongoCollection<Document>?) {
init {
collection.createIndex(Indexes.ascending("username"), IndexOptions().unique(true))
collection?.createIndex(Indexes.ascending("username"), IndexOptions().unique(true))
}
fun getUser(username: String): UserMetadata? {
return collection
.find(Filters.eq("username", username))
.map { UserMetadata(it) }
.first()
?.find(Filters.eq("username", username))
?.map { UserMetadata(it) }
?.first()
}
}
@@ -1,410 +0,0 @@
package com.projectswg.holocore.services.gameplay.combat.loot;
import com.projectswg.common.data.location.Location;
import com.projectswg.common.network.packets.swg.zone.PlayClientEffectObjectTransformMessage;
import com.projectswg.holocore.intents.gameplay.combat.CreatureKilledIntent;
import com.projectswg.holocore.intents.gameplay.combat.loot.CorpseLootedIntent;
import com.projectswg.holocore.intents.gameplay.combat.loot.LootGeneratedIntent;
import com.projectswg.holocore.intents.support.global.chat.SystemMessageIntent;
import com.projectswg.holocore.intents.support.objects.items.CreateStaticItemIntent;
import com.projectswg.holocore.intents.support.objects.swg.ObjectCreatedIntent;
import com.projectswg.holocore.resources.support.data.server_info.SdbColumnArraySet.SdbIntegerColumnArraySet;
import com.projectswg.holocore.resources.support.data.server_info.SdbColumnArraySet.SdbTextColumnArraySet;
import com.projectswg.holocore.resources.support.data.server_info.SdbLoader;
import com.projectswg.holocore.resources.support.data.server_info.SdbLoader.SdbResultSet;
import com.projectswg.holocore.resources.support.data.server_info.StandardLog;
import com.projectswg.holocore.resources.support.data.server_info.loader.DataLoader;
import com.projectswg.holocore.resources.support.data.server_info.loader.npc.NpcLoader;
import com.projectswg.holocore.resources.support.data.server_info.loader.npc.NpcLoader.NpcInfo;
import com.projectswg.holocore.resources.support.data.server_info.mongodb.PswgDatabase;
import com.projectswg.holocore.resources.support.objects.ObjectCreator;
import com.projectswg.holocore.resources.support.objects.swg.SWGObject;
import com.projectswg.holocore.resources.support.objects.swg.creature.CreatureDifficulty;
import com.projectswg.holocore.resources.support.objects.swg.creature.CreatureObject;
import com.projectswg.holocore.resources.support.objects.swg.custom.AIObject;
import com.projectswg.holocore.resources.support.objects.swg.group.GroupObject;
import com.projectswg.holocore.resources.support.objects.swg.tangible.CreditObject;
import com.projectswg.holocore.services.support.objects.ObjectStorageService.ObjectLookup;
import com.projectswg.holocore.services.support.objects.items.StaticItemService;
import me.joshlarson.jlcommon.control.IntentChain;
import me.joshlarson.jlcommon.control.IntentHandler;
import me.joshlarson.jlcommon.control.Service;
import me.joshlarson.jlcommon.log.Log;
import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.ThreadLocalRandom;
public final class LootGenerationService extends Service {
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 LootGenerationService() {
lootTables = new HashMap<>();
npcLoot = new HashMap<>();
random = new Random();
}
@Override
public boolean initialize() {
loadLootTables();
loadNPCLoot();
return super.initialize();
}
@IntentHandler
private void handleCreatureKilled(CreatureKilledIntent cki) {
AIObject corpse;
{
CreatureObject corpseRaw = cki.getCorpse();
if (corpseRaw.isPlayer()) {
// Players don't drop loot
return;
}
if (!(corpseRaw instanceof AIObject))
return;
corpse = (AIObject) corpseRaw;
}
String creatureId = 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.moveToContainer(corpse, corpse.getLocation());
ObjectCreatedIntent.broadcast(lootInventory);
CreatureObject killer = cki.getKiller();
boolean cashGenerated = false;
boolean lootGenerated = false;
if (PswgDatabase.INSTANCE.getConfig().getBoolean(this, "cashLoot", true))
cashGenerated = generateCreditChip(loot, lootInventory, corpse.getDifficulty(), corpse.getLevel());
if (PswgDatabase.INSTANCE.getConfig().getBoolean(this, "itemLoot", true))
lootGenerated = generateLoot(loot, killer, lootInventory);
if (!cashGenerated && !lootGenerated)
new CorpseLootedIntent(corpse).broadcast();
else
showLootDisc(killer, corpse);
}
/**
* Loads loot tables from loot_table.db. Each "loot table" is in the form of a row. Each row has an id and several "loot groups," which consist of actual items that can be dropped. Each group has
* a different chance of being selected for loot.
*/
private void loadLootTables() {
long startTime = StandardLog.onStartLoad("loot tables");
try (SdbResultSet set = SdbLoader.load(new File("serverdata/loot/loot_table.sdb"))) {
SdbTextColumnArraySet itemGroups = set.getTextArrayParser("items_group_([0-9]+)");
SdbIntegerColumnArraySet chanceGroups = set.getIntegerArrayParser("chance_group_([0-9]+)");
while (set.next()) {
String tableName = set.getText("loot_id");
if (tableName.equals("-"))
continue;
LootTable table = new LootTable();
int totalChance = 0; // Must not be above 100. Also used to convert chances in the form of "33, 33, 34" to "33, 66, 100"
String [] itemGroupsArray = itemGroups.getArray(set);
int [] chanceGroupsArray = chanceGroups.getArray(set);
assert itemGroupsArray.length == chanceGroupsArray.length;
for (int groupNum = 0; groupNum < chanceGroupsArray.length && totalChance < 100; groupNum++) {
int groupChance = chanceGroupsArray[groupNum];
if (groupChance == 0 || groupChance == Integer.MAX_VALUE)
continue;
groupChance += totalChance;
table.addLootGroup(new LootGroup(groupChance, itemGroupsArray[groupNum].split(";")));
totalChance = groupChance;
}
if (totalChance != 100)
Log.w("Loot chance != 100 while loading loot_table! Table: %s, totalChance: %s", tableName, totalChance);
lootTables.put(tableName, table);
}
} catch (IOException e) {
Log.e(e);
}
StandardLog.onEndLoad(lootTables.size(), "loot tables", startTime);
}
/**
* Loads NPC loot tables. There are up to 3 tables per NPC.
*/
private void loadNPCLoot() {
long startTime = StandardLog.onStartLoad("NPC loot links");
NpcLoader npcLoader = DataLoader.npcs();
for (NpcInfo info : npcLoader.getNpcs()) {
NPCLoot loot = new NPCLoot(info.getHumanoidInfo() != null);
// load each loot table (up to 3) and add to loot object
loadNPCTable(loot, info.getLootTable1(), info.getLootTable1Chance());
loadNPCTable(loot, info.getLootTable2(), info.getLootTable2Chance());
loadNPCTable(loot, info.getLootTable3(), info.getLootTable3Chance());
npcLoot.put(info.getId(), loot);
}
StandardLog.onEndLoad(npcLoot.size(), "NPC loot links", startTime);
}
/**
* Load a specific table for an NPC.
*
* @param loot the loot object for the NPC
* @param table the loot table
* @param chance the chance for this loot table (used when generating loot)
*/
private void loadNPCTable(NPCLoot loot, String table, int chance) {
// if chance <= 0, this table for this NPC doesn't exist
if (chance <= 0)
return;
LootTable lootTable = lootTables.get(table);
if (lootTable == null) {
Log.w("Loot table not found: %s", table);
return;
}
loot.addNPCTable(new NPCTable(chance, lootTable));
}
private void showLootDisc(CreatureObject requester, AIObject corpse) {
assert requester.isPlayer();
Location effectLocation = Location.builder(corpse.getLocation()).setPosition(0, 0.5, 0).build();
long requesterGroup = requester.getGroupId();
if (requesterGroup != 0) {
GroupObject requesterGroupObject = (GroupObject) ObjectLookup.getObjectById(requesterGroup);
assert requesterGroupObject != null;
for (CreatureObject creature : requesterGroupObject.getGroupMemberObjects()) {
creature.sendSelf(new PlayClientEffectObjectTransformMessage(corpse.getObjectId(), "appearance/pt_loot_disc.prt", effectLocation, "lootMe"));
}
} else {
requester.sendSelf(new PlayClientEffectObjectTransformMessage(corpse.getObjectId(), "appearance/pt_loot_disc.prt", effectLocation, "lootMe"));
}
LootGeneratedIntent.broadcast(corpse);
}
private boolean generateCreditChip(NPCLoot loot, SWGObject lootInventory, CreatureDifficulty difficulty, int combatLevel) {
if (!loot.isDropCredits())
return false;
ThreadLocalRandom random = ThreadLocalRandom.current();
double range = PswgDatabase.INSTANCE.getConfig().getDouble(this, "lootCashHumanRange", 0.05);
double cashLootRoll = random.nextDouble();
int credits;
switch (difficulty) {
default:
case NORMAL:
if (cashLootRoll > PswgDatabase.INSTANCE.getConfig().getDouble(this, "lootCashHumanNormalChance", 0.60))
return false;
credits = PswgDatabase.INSTANCE.getConfig().getInt(this, "lootCashHumanNormal", 2);
break;
case ELITE:
if (cashLootRoll > PswgDatabase.INSTANCE.getConfig().getDouble(this, "lootCashHumanElitechance", 0.80))
return false;
credits = PswgDatabase.INSTANCE.getConfig().getInt(this, "lootCashHumanElite", 5);
break;
case BOSS:
// bosses always drop cash loot, so no need to check
credits = PswgDatabase.INSTANCE.getConfig().getInt(this, "lootCashHumanBoss", 9);
break;
}
credits *= combatLevel;
credits *= 1 + random.nextDouble(0, 2*range-range);
// TODO scale with group size?
CreditObject cashObject = ObjectCreator.createObjectFromTemplate("object/tangible/item/shared_loot_cash.iff", CreditObject.class);
cashObject.setAmount(credits);
cashObject.moveToContainer(lootInventory);
ObjectCreatedIntent.broadcast(cashObject);
return true;
}
/**
* Generates loot and places it in the inventory of the corpse.
*
* @param loot the loot info of the creature killed
* @param killer the person that killed the NPC
* @param lootInventory the inventory the loot will be placed in (corpse inventory)
* @return whether loot was generated or not
*/
private boolean generateLoot(NPCLoot loot, CreatureObject killer, SWGObject lootInventory) {
boolean lootGenerated = false;
int tableRoll = random.nextInt(100) + 1;
// Admin Variables
boolean admin = killer.hasCommand("admin");
StringBuilder adminOutput1 = new StringBuilder(tableRoll + " //");
StringBuilder adminOutput2 = new StringBuilder();
int tableId = 0;
for (NPCTable npcTable : loot.getNPCTables()) {
LootTable lootTable = npcTable.getLootTable();
int tableChance = npcTable.getChance();
tableId++;
if (tableRoll > tableChance) {
// Skip ahead if there's no drop chance
adminOutput1.append("/ \\#FF0000 loot_table").append(tableId);
continue;
}
adminOutput1.append("/ \\#00FF00 loot_table").append(tableId);
int groupRoll = random.nextInt(100) + 1;
for (LootGroup group : lootTable.getLootGroups()) {
int groupChance = group.getChance();
if (groupRoll > groupChance)
continue;
String[] itemNames = group.getItemNames();
String itemName = itemNames[random.nextInt(itemNames.length)];
adminOutput2.append(itemName).append('\n');
if (itemName.startsWith("dynamic_")) {
// TODO dynamic item handling
new SystemMessageIntent(killer.getOwner(), "We don't support this loot item yet: " + itemName).broadcast();
} else if (itemName.endsWith(".iff")) {
SWGObject object = ObjectCreator.createObjectFromTemplate(itemName);
object.moveToContainer(lootInventory);
ObjectCreatedIntent.broadcast(object);
lootGenerated = true;
} else {
new CreateStaticItemIntent(killer, lootInventory, new CreateStaticItemCallback(), itemName).broadcast();
lootGenerated = true;
}
break;
}
}
if (admin) {
IntentChain.broadcastChain(
new SystemMessageIntent(killer.getOwner(), adminOutput1.toString()),
new SystemMessageIntent(killer.getOwner(), adminOutput2.toString())
);
}
return lootGenerated;
}
private static class NPCLoot {
private final Collection<NPCTable> npcTables;
private final boolean canDropCredits;
public NPCLoot(boolean canDropCredits) {
this.npcTables = new ArrayList<>();
this.canDropCredits = canDropCredits;
}
public boolean isDropCredits() {
return canDropCredits;
}
public Collection<NPCTable> getNPCTables() {
return Collections.unmodifiableCollection(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;
}
}
private static class CreateStaticItemCallback implements StaticItemService.ObjectCreationHandler {
@Override
public void success(List<SWGObject> createdObjects) {
// do nothing - loot disc is created on the return of the generateLoot method
}
@Override
public boolean isIgnoreVolume() {
return true;
}
}
}
@@ -0,0 +1,249 @@
package com.projectswg.holocore.services.gameplay.combat.loot
import com.projectswg.common.data.location.Location
import com.projectswg.common.network.packets.swg.zone.PlayClientEffectObjectTransformMessage
import com.projectswg.holocore.intents.gameplay.combat.CreatureKilledIntent
import com.projectswg.holocore.intents.gameplay.combat.loot.CorpseLootedIntent
import com.projectswg.holocore.intents.gameplay.combat.loot.LootGeneratedIntent
import com.projectswg.holocore.intents.support.global.chat.SystemMessageIntent
import com.projectswg.holocore.intents.support.objects.items.CreateStaticItemIntent
import com.projectswg.holocore.intents.support.objects.swg.ObjectCreatedIntent
import com.projectswg.holocore.resources.support.data.server_info.StandardLog
import com.projectswg.holocore.resources.support.data.server_info.loader.DataLoader
import com.projectswg.holocore.resources.support.data.server_info.mongodb.PswgDatabase
import com.projectswg.holocore.resources.support.objects.ObjectCreator
import com.projectswg.holocore.resources.support.objects.swg.SWGObject
import com.projectswg.holocore.resources.support.objects.swg.creature.CreatureDifficulty
import com.projectswg.holocore.resources.support.objects.swg.creature.CreatureObject
import com.projectswg.holocore.resources.support.objects.swg.custom.AIObject
import com.projectswg.holocore.resources.support.objects.swg.group.GroupObject
import com.projectswg.holocore.resources.support.objects.swg.tangible.CreditObject
import com.projectswg.holocore.services.support.objects.ObjectStorageService.ObjectLookup
import com.projectswg.holocore.services.support.objects.items.StaticItemService
import me.joshlarson.jlcommon.control.IntentChain
import me.joshlarson.jlcommon.control.IntentHandler
import me.joshlarson.jlcommon.control.Service
import me.joshlarson.jlcommon.log.Log
import java.util.*
import java.util.concurrent.ThreadLocalRandom
class LootGenerationService : Service() {
private val npcLoot: MutableMap<String, NPCLoot> = HashMap() // K: npc_id, V: possible loot
override fun initialize(): Boolean {
loadNPCLoot()
return true
}
@IntentHandler
private fun handleCreatureKilled(cki: CreatureKilledIntent) {
val corpse = cki.corpse as? AIObject ?: return
val creatureId = corpse.creatureId
val loot = npcLoot[creatureId]
if (loot == null) {
Log.w("No NPCLoot associated with NPC ID: $creatureId")
return
}
val lootInventory = ObjectCreator.createObjectFromTemplate("object/tangible/inventory/shared_creature_inventory.iff")
lootInventory.moveToContainer(corpse, corpse.location)
ObjectCreatedIntent.broadcast(lootInventory)
val killer = cki.killer
var cashGenerated = false
var lootGenerated = false
if (PswgDatabase.config.getBoolean(this, "cashLoot", true))
cashGenerated = generateCreditChip(loot, lootInventory, corpse.difficulty, corpse.level.toInt())
if (PswgDatabase.config.getBoolean(this, "itemLoot", true))
lootGenerated = generateLoot(loot, killer, lootInventory, corpse.difficulty, corpse.level.toInt())
if (!cashGenerated && !lootGenerated)
CorpseLootedIntent(corpse).broadcast()
else
showLootDisc(killer, corpse)
}
/**
* Loads NPC loot tables. There are up to 3 tables per NPC.
*/
private fun loadNPCLoot() {
val startTime = StandardLog.onStartLoad("NPC loot links")
val npcLoader = DataLoader.npcs()
for (info in npcLoader.npcs) {
val loot = NPCLoot(info.humanoidInfo != null)
// load each loot table (up to 3) and add to loot object
loadNPCTable(loot, info.lootTable1, info.lootTable1Chance)
loadNPCTable(loot, info.lootTable2, info.lootTable2Chance)
loadNPCTable(loot, info.lootTable3, info.lootTable3Chance)
npcLoot[info.id] = loot
}
StandardLog.onEndLoad(npcLoot.size, "NPC loot links", startTime)
}
/**
* Load a specific table for an NPC.
*
* @param loot the loot object for the NPC
* @param table the loot table
* @param chance the chance for this loot table (used when generating loot)
*/
private fun loadNPCTable(loot: NPCLoot, table: String, chance: Int) {
// if chance <= 0, this table for this NPC doesn't exist
if (chance <= 0)
return
loot.npcTables.add(NPCTable(chance, table))
}
private fun showLootDisc(requester: CreatureObject, corpse: AIObject) {
assert(requester.isPlayer)
val effectLocation = Location.builder(corpse.location).setPosition(0.0, 0.5, 0.0).build()
val requesterGroup = requester.groupId
if (requesterGroup != 0L) {
val requesterGroupObject = (ObjectLookup.getObjectById(requesterGroup) as GroupObject?)!!
for (creature in requesterGroupObject.groupMemberObjects) {
creature.sendSelf(PlayClientEffectObjectTransformMessage(corpse.objectId, "appearance/pt_loot_disc.prt", effectLocation, "lootMe"))
}
} else {
requester.sendSelf(PlayClientEffectObjectTransformMessage(corpse.objectId, "appearance/pt_loot_disc.prt", effectLocation, "lootMe"))
}
LootGeneratedIntent.broadcast(corpse)
}
private fun generateCreditChip(loot: NPCLoot, lootInventory: SWGObject, difficulty: CreatureDifficulty, combatLevel: Int): Boolean {
if (!loot.isDropCredits)
return false
val random = ThreadLocalRandom.current()
val config = PswgDatabase.config
val range = config.getDouble(this, "lootCashHumanRange", 0.05)
val cashLootRoll = random.nextDouble()
val credits = combatLevel * (1 + random.nextDouble(0.0, range)).toInt() * when (difficulty) {
CreatureDifficulty.NORMAL -> {
if (cashLootRoll > config.getDouble(this, "lootCashHumanNormalChance", 0.60))
return false
config.getInt(this, "lootCashHumanNormal", 2)
}
CreatureDifficulty.ELITE -> {
if (cashLootRoll > config.getDouble(this, "lootCashHumanElitechance", 0.80))
return false
config.getInt(this, "lootCashHumanElite", 5)
}
CreatureDifficulty.BOSS -> // bosses always drop cash loot, so no need to check
config.getInt(this, "lootCashHumanBoss", 9)
}
// TODO scale with group size?
val cashObject = ObjectCreator.createObjectFromTemplate("object/tangible/item/shared_loot_cash.iff", CreditObject::class.java)
cashObject.amount = credits.toLong()
cashObject.moveToContainer(lootInventory)
ObjectCreatedIntent.broadcast(cashObject)
return true
}
/**
* Generates loot and places it in the inventory of the corpse.
*
* @param loot the loot info of the creature killed
* @param killer the person that killed the NPC
* @param lootInventory the inventory the loot will be placed in (corpse inventory)
* @return whether loot was generated or not
*/
private fun generateLoot(loot: NPCLoot, killer: CreatureObject, lootInventory: SWGObject, difficulty: CreatureDifficulty, level: Int): Boolean {
val random = ThreadLocalRandom.current()
var lootGenerated = false
val tableRoll = random.nextInt(100) + 1
// Admin Variables
val admin = killer.hasCommand("admin")
val adminOutput1 = StringBuilder("$tableRoll //")
val adminOutput2 = StringBuilder()
for ((tableId, npcTable) in loot.npcTables.withIndex()) {
val tableChance = npcTable.chance
if (tableRoll > tableChance) {
// Skip ahead if there's no drop chance
adminOutput1.append("/ \\#FF0000 loot_table").append(tableId)
break
}
adminOutput1.append("/ \\#00FF00 loot_table").append(tableId)
val groupRoll = random.nextInt(100) + 1
Log.d("Loot Table Lookup: table=${npcTable.lootTable} difficulty=${difficulty.name} level=$level")
Log.d("Loot Table Item: ${DataLoader.lootTables().getLootTableItem(npcTable.lootTable, difficulty.name, level)} groupRoll=$groupRoll")
val lootTableItem = DataLoader.lootTables().getLootTableItem(npcTable.lootTable, difficulty.name, level) ?: continue
for ((chance, items) in lootTableItem.groups) { // group of items to be granted
if (groupRoll > chance)
continue
val itemName = items[random.nextInt(items.size)]
adminOutput2.append(itemName).append('\n')
when {
itemName.startsWith("dynamic_") -> { // TODO dynamic item handling
SystemMessageIntent(killer.owner!!, "We don't support this loot item yet: $itemName").broadcast()
}
itemName.endsWith(".iff") -> {
val obj = ObjectCreator.createObjectFromTemplate(itemName)
obj.moveToContainer(lootInventory)
ObjectCreatedIntent.broadcast(obj)
lootGenerated = true
}
else -> {
CreateStaticItemIntent(killer, lootInventory, CreateStaticItemCallback(), itemName).broadcast()
lootGenerated = true
}
}
break
}
}
if (admin) {
IntentChain.broadcastChain(
SystemMessageIntent(killer.owner!!, adminOutput1.toString()),
SystemMessageIntent(killer.owner!!, adminOutput2.toString())
)
}
return lootGenerated
}
private class NPCLoot(val isDropCredits: Boolean, val npcTables: MutableCollection<NPCTable> = ArrayList())
private class NPCTable(val chance: Int, val lootTable: String)
private class CreateStaticItemCallback : StaticItemService.ObjectCreationHandler {
override fun success(createdObjects: List<SWGObject>) {
// do nothing - loot disc is created on the return of the generateLoot method
}
override fun isIgnoreVolume(): Boolean {
return true
}
}
}