mirror of
https://github.com/ProjectSWGCore/Holocore.git
synced 2026-10-10 00:13:42 -04:00
Converted databases to null-safe delegates and converted LootGenerationService to kotlin
This commit is contained in:
+1
-1
@@ -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;
|
||||
|
||||
+4
@@ -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);
|
||||
}
|
||||
|
||||
+96
@@ -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>)
|
||||
|
||||
}
|
||||
+7
-19
@@ -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())
|
||||
|
||||
}
|
||||
|
||||
|
||||
+28
-17
@@ -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)
|
||||
|
||||
}
|
||||
|
||||
+9
-3
@@ -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
|
||||
}
|
||||
|
||||
|
||||
+4
-4
@@ -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))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+5
-5
@@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
-410
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+249
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user