From c60dedfa9982df80212b577b2034f408dd9d8b76 Mon Sep 17 00:00:00 2001 From: Obique Date: Sun, 19 May 2019 12:35:23 -0500 Subject: [PATCH] Converted databases to null-safe delegates and converted LootGenerationService to kotlin --- .../support/data/server_info/SdbLoader.java | 2 +- .../data/server_info/loader/DataLoader.java | 4 + .../server_info/loader/npc/LootTableLoader.kt | 96 ++++ .../loader/npc/NpcCombatProfileLoader.kt | 26 +- .../data/server_info/mongodb/PswgDatabase.kt | 45 +- .../server_info/mongodb/PswgObjectDatabase.kt | 12 +- .../mongodb/PswgResourceDatabase.kt | 8 +- .../server_info/mongodb/PswgUserDatabase.kt | 10 +- .../combat/loot/LootGenerationService.java | 410 ------------------ .../combat/loot/LootGenerationService.kt | 249 +++++++++++ 10 files changed, 403 insertions(+), 459 deletions(-) create mode 100644 src/main/java/com/projectswg/holocore/resources/support/data/server_info/loader/npc/LootTableLoader.kt delete mode 100644 src/main/java/com/projectswg/holocore/services/gameplay/combat/loot/LootGenerationService.java create mode 100644 src/main/java/com/projectswg/holocore/services/gameplay/combat/loot/LootGenerationService.kt diff --git a/src/main/java/com/projectswg/holocore/resources/support/data/server_info/SdbLoader.java b/src/main/java/com/projectswg/holocore/resources/support/data/server_info/SdbLoader.java index 0fb140809..ca00716a3 100644 --- a/src/main/java/com/projectswg/holocore/resources/support/data/server_info/SdbLoader.java +++ b/src/main/java/com/projectswg/holocore/resources/support/data/server_info/SdbLoader.java @@ -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; diff --git a/src/main/java/com/projectswg/holocore/resources/support/data/server_info/loader/DataLoader.java b/src/main/java/com/projectswg/holocore/resources/support/data/server_info/loader/DataLoader.java index c8c5e38a9..40f2ae374 100644 --- a/src/main/java/com/projectswg/holocore/resources/support/data/server_info/loader/DataLoader.java +++ b/src/main/java/com/projectswg/holocore/resources/support/data/server_info/loader/DataLoader.java @@ -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); } diff --git a/src/main/java/com/projectswg/holocore/resources/support/data/server_info/loader/npc/LootTableLoader.kt b/src/main/java/com/projectswg/holocore/resources/support/data/server_info/loader/npc/LootTableLoader.kt new file mode 100644 index 000000000..d526e58b2 --- /dev/null +++ b/src/main/java/com/projectswg/holocore/resources/support/data/server_info/loader/npc/LootTableLoader.kt @@ -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 . * + ***********************************************************************************/ + +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>> = 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 + + init { + val groups = itemGroups.getArray(set) + val chances = chanceGroups.getArray(set) + val count = min(groups.size, chances.size) + + val groupList = ArrayList() + 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) + +} diff --git a/src/main/java/com/projectswg/holocore/resources/support/data/server_info/loader/npc/NpcCombatProfileLoader.kt b/src/main/java/com/projectswg/holocore/resources/support/data/server_info/loader/npc/NpcCombatProfileLoader.kt index f71a3b837..00e74f342 100644 --- a/src/main/java/com/projectswg/holocore/resources/support/data/server_info/loader/npc/NpcCombatProfileLoader.kt +++ b/src/main/java/com/projectswg/holocore/resources/support/data/server_info/loader/npc/NpcCombatProfileLoader.kt @@ -40,11 +40,7 @@ import java.util.stream.Collectors.groupingBy class NpcCombatProfileLoader : DataLoader() { - private val profiles: MutableMap> - - init { - this.profiles = HashMap() - } + private val profiles: MutableMap> = HashMap() fun getAbilities(id: String?, difficulty: CreatureDifficulty, level: Int): Set { val subprofiles = Objects.requireNonNull>(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(Function { CombatProfile(it) }).collect>, Any>(groupingBy(Function { 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 - - 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(*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()) } diff --git a/src/main/java/com/projectswg/holocore/resources/support/data/server_info/mongodb/PswgDatabase.kt b/src/main/java/com/projectswg/holocore/resources/support/data/server_info/mongodb/PswgDatabase.kt index ea88f9c83..6a4c8a71a 100644 --- a/src/main/java/com/projectswg/holocore/resources/support/data/server_info/mongodb/PswgDatabase.kt +++ b/src/main/java/com/projectswg/holocore/resources/support/data/server_info/mongodb/PswgDatabase.kt @@ -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(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(private val database: ObjectRef, private val collectionName: String, private var databaseSupplier: (MongoCollection?) -> 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(var element: T) + } diff --git a/src/main/java/com/projectswg/holocore/resources/support/data/server_info/mongodb/PswgObjectDatabase.kt b/src/main/java/com/projectswg/holocore/resources/support/data/server_info/mongodb/PswgObjectDatabase.kt index 5b7be130d..222a284ba 100644 --- a/src/main/java/com/projectswg/holocore/resources/support/data/server_info/mongodb/PswgObjectDatabase.kt +++ b/src/main/java/com/projectswg/holocore/resources/support/data/server_info/mongodb/PswgObjectDatabase.kt @@ -37,20 +37,22 @@ import java.util.* import java.util.regex.Pattern import java.util.stream.Collectors.toList -class PswgObjectDatabase(private val collection: MongoCollection) { +class PswgObjectDatabase(private val collection: MongoCollection?) { val objects: List - 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) { + collection ?: return if (objects.isEmpty()) return collection.bulkWrite(objects.stream() @@ -66,14 +68,17 @@ class PswgObjectDatabase(private val collection: MongoCollection) { } 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) { } fun clearObjects(): Long { + collection ?: return 0 return collection.deleteMany(Filters.exists("_id")).deletedCount } diff --git a/src/main/java/com/projectswg/holocore/resources/support/data/server_info/mongodb/PswgResourceDatabase.kt b/src/main/java/com/projectswg/holocore/resources/support/data/server_info/mongodb/PswgResourceDatabase.kt index 1afbf8b48..878541f02 100644 --- a/src/main/java/com/projectswg/holocore/resources/support/data/server_info/mongodb/PswgResourceDatabase.kt +++ b/src/main/java/com/projectswg/holocore/resources/support/data/server_info/mongodb/PswgResourceDatabase.kt @@ -35,12 +35,12 @@ import org.bson.Document import java.util.* import java.util.stream.Collectors.toList -class PswgResourceDatabase(private val collection: MongoCollection) { +class PswgResourceDatabase(private val collection: MongoCollection?) { var resources: List - 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) { } init { - collection.createIndex(Indexes.ascending("id"), IndexOptions().unique(true)) + collection?.createIndex(Indexes.ascending("id"), IndexOptions().unique(true)) } } diff --git a/src/main/java/com/projectswg/holocore/resources/support/data/server_info/mongodb/PswgUserDatabase.kt b/src/main/java/com/projectswg/holocore/resources/support/data/server_info/mongodb/PswgUserDatabase.kt index 498ea366c..d3e5b2dc9 100644 --- a/src/main/java/com/projectswg/holocore/resources/support/data/server_info/mongodb/PswgUserDatabase.kt +++ b/src/main/java/com/projectswg/holocore/resources/support/data/server_info/mongodb/PswgUserDatabase.kt @@ -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) { +class PswgUserDatabase(private val collection: MongoCollection?) { 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() } } diff --git a/src/main/java/com/projectswg/holocore/services/gameplay/combat/loot/LootGenerationService.java b/src/main/java/com/projectswg/holocore/services/gameplay/combat/loot/LootGenerationService.java deleted file mode 100644 index fcee61fcc..000000000 --- a/src/main/java/com/projectswg/holocore/services/gameplay/combat/loot/LootGenerationService.java +++ /dev/null @@ -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 lootTables; // K: loot_id, V: table contents - private final Map 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 npcTables; - private final boolean canDropCredits; - - public NPCLoot(boolean canDropCredits) { - this.npcTables = new ArrayList<>(); - this.canDropCredits = canDropCredits; - } - - public boolean isDropCredits() { - return canDropCredits; - } - - public Collection 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 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; - } - } - - private static class CreateStaticItemCallback implements StaticItemService.ObjectCreationHandler { - - @Override - public void success(List createdObjects) { - // do nothing - loot disc is created on the return of the generateLoot method - } - - @Override - public boolean isIgnoreVolume() { - return true; - } - } - -} diff --git a/src/main/java/com/projectswg/holocore/services/gameplay/combat/loot/LootGenerationService.kt b/src/main/java/com/projectswg/holocore/services/gameplay/combat/loot/LootGenerationService.kt new file mode 100644 index 000000000..1e6e7f957 --- /dev/null +++ b/src/main/java/com/projectswg/holocore/services/gameplay/combat/loot/LootGenerationService.kt @@ -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 = 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 = ArrayList()) + + private class NPCTable(val chance: Int, val lootTable: String) + + private class CreateStaticItemCallback : StaticItemService.ObjectCreationHandler { + + override fun success(createdObjects: List) { + // do nothing - loot disc is created on the return of the generateLoot method + } + + override fun isIgnoreVolume(): Boolean { + return true + } + } + +}