Skip to content

Commit

Permalink
Ensure player inventory reflects cards + items that are removed from …
Browse files Browse the repository at this point in the history
…their deck

Prior to this commit; if a key is consumed in the dungeon but was
somehow still in the player's inventory in the lobby, then the key will
still remain in the player's lobby inventory when they reconnect. This
was because Citadel did not remove items/cards from the player's
inventory if they were removed from the deck.

Relates to #45
  • Loading branch information
4Ply committed Jan 1, 2025
1 parent 389a80e commit c71e3e0
Show file tree
Hide file tree
Showing 4 changed files with 267 additions and 1 deletion.
2 changes: 1 addition & 1 deletion src/main/kotlin/org/trackedout/citadel/Citadel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ class Citadel : JavaPlugin() {
.build()
)

val inventoryManager = InventoryManager(this, scoreApi, eventsApi)
val inventoryManager = InventoryManager(this, inventoryApi, scoreApi, eventsApi)

MongoDBManager.initialize(mongoURI)

Expand Down
47 changes: 47 additions & 0 deletions src/main/kotlin/org/trackedout/citadel/InventoryManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,28 @@ package org.trackedout.citadel
import org.bukkit.Material
import org.bukkit.entity.Player
import org.bukkit.inventory.ItemStack
import org.trackedout.citadel.commands.GiveShulkerCommand.Companion.createCard
import org.trackedout.citadel.commands.ScoreManagementCommand
import org.trackedout.citadel.inventory.ScoreboardDescriber
import org.trackedout.citadel.inventory.Trade
import org.trackedout.citadel.inventory.baseTradeItems
import org.trackedout.citadel.inventory.competitiveDeck
import org.trackedout.citadel.inventory.intoDungeonItems
import org.trackedout.citadel.inventory.oldDungeonItem
import org.trackedout.citadel.inventory.practiceDeck
import org.trackedout.citadel.inventory.shortRunType
import org.trackedout.citadel.inventory.tradeItems
import org.trackedout.citadel.inventory.withTradeMeta
import org.trackedout.client.apis.EventsApi
import org.trackedout.client.apis.InventoryApi
import org.trackedout.client.apis.ScoreApi
import org.trackedout.client.models.Event
import org.trackedout.client.models.Score
import org.trackedout.data.Cards

class InventoryManager(
private val plugin: Citadel,
private val inventoryApi: InventoryApi,
private val scoreApi: ScoreApi,
private val eventsApi: EventsApi,
) {
Expand All @@ -40,7 +46,46 @@ class InventoryManager(
score.value!!.toInt()
)
}

ensurePlayerInventoryReflectsItemsOutsideOfDeck(player)
}
}

private fun ensurePlayerInventoryReflectsItemsOutsideOfDeck(player: Player) {
// For each card in the player's inventory, ensure they only have the correct amount
val deckItems = inventoryApi.inventoryCardsGet(player = player.name, limit = 200).results!!

ScoreManagementCommand.RunTypes.entries.forEach { runType ->

// Check cards against contents of player's deck
Cards.Companion.Card.entries.sortedBy { it.colour + it.key }.forEach { card ->
val maxCardsThatShouldBeInInventory = deckItems.count {
plugin.logger.info("Checking ${it.name} == ${card.key} && ${it.deckType} == ${runType.runType.shortRunType()} && ${it.hiddenInDecks?.isNotEmpty() == true}")
it.name == card.key && it.deckType == runType.runType.shortRunType() && it.hiddenInDecks?.isNotEmpty() == true
}

plugin.logger.info("${player.name} should have ${maxCardsThatShouldBeInInventory}x${card.key} in their inventory (runType: ${runType.runType})")
val itemStack = createCard(plugin, null, card.key, 1, "${runType.runType.shortRunType()}1")

itemStack?.let {
player.ensureInventoryContains(it.clone().apply { amount = zeroSupportedItemCount(maxCardsThatShouldBeInInventory) })
}
}

// Check items against contents of player's deck
intoDungeonItems.entries.forEach { (itemKey, scoreboardDescriber) ->
val maxItemsThatShouldBeInInventory = deckItems.count {
it.name == itemKey && it.deckType == runType.runType.shortRunType() && it.hiddenInDecks?.isNotEmpty() == true
}
plugin.logger.info("${player.name} should have ${maxItemsThatShouldBeInInventory}x${itemKey} in their inventory (runType: ${runType.runType})")
val itemStack = scoreboardDescriber.itemStack(runType.runType, 1)

itemStack.let {
player.ensureInventoryContains(it.withTradeMeta(runType.runType, itemKey).clone().apply { amount = zeroSupportedItemCount(maxItemsThatShouldBeInInventory) })
}
}
}

}

private fun ensurePlayerHasPracticeShards(player: Player, currentScores: List<Score>): List<Score> {
Expand Down Expand Up @@ -86,6 +131,8 @@ class InventoryManager(
return scores
}

private fun zeroSupportedItemCount(count: Int) = if (count <= 0) 999 else count

private fun cleanUpOldItems(player: Player) {
plugin.logger.info("Cleaning up old items for ${player.name}")
for (runType in listOf("practice", "competitive")) {
Expand Down
200 changes: 200 additions & 0 deletions src/main/kotlin/org/trackedout/citadel/commands/ConfigCommand.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
package org.trackedout.citadel.commands

import co.aikar.commands.BaseCommand
import co.aikar.commands.annotation.CommandAlias
import co.aikar.commands.annotation.CommandPermission
import co.aikar.commands.annotation.Description
import co.aikar.commands.annotation.Subcommand
import com.mongodb.client.model.Filters
import com.mongodb.client.model.Filters.eq
import me.devnatan.inventoryframework.ViewFrame
import net.kyori.adventure.text.Component
import net.kyori.adventure.text.event.ClickEvent
import net.kyori.adventure.text.event.HoverEvent
import net.kyori.adventure.text.format.NamedTextColor
import net.kyori.adventure.text.minimessage.MiniMessage
import org.bukkit.command.CommandSender
import org.bukkit.entity.Player
import org.trackedout.citadel.Citadel
import org.trackedout.citadel.async
import org.trackedout.citadel.commands.ScoreManagementCommand.RunTypes
import org.trackedout.citadel.config.scoreboardMap
import org.trackedout.citadel.mongo.MongoDBManager
import org.trackedout.citadel.mongo.MongoPlayerStats
import org.trackedout.citadel.sendRedMessage
import org.trackedout.client.apis.EventsApi
import org.trackedout.client.apis.ScoreApi
import org.trackedout.client.models.Score
import org.trackedout.data.BrillianceScoreboardDescription
import java.math.BigDecimal

@CommandAlias("decked-out|do")
class ConfigCommand(
private val plugin: Citadel,
private val eventsApi: EventsApi,
private val scoreApi: ScoreApi,
private val viewFrame: ViewFrame,
) : BaseCommand() {

@Subcommand("config list")
@Description("List config values")
fun showConfig(source: CommandSender, runType: RunTypes) {
if (source is Player) {
source.sendConfigList(runType, source.name)
} else {
source.sendRedMessage("Cannot list your configs as you are not a player. Use /do config list <playerName>")
}
}

@Subcommand("config list")
@CommandPermission("decked-out.config.view.all")
@Description("List config values for target player")
fun showConfigForPlayer(source: CommandSender, runType: RunTypes, targetPlayer: String) {
source.sendConfigList(runType, targetPlayer)
}

private fun CommandSender.sendConfigList(runType: RunTypes, targetPlayer: String) {
val mm = MiniMessage.miniMessage()

plugin.async(this) {
val scores = scoreApi.scoresGet(player = targetPlayer).results!!

val applicableScores = scores.filter { isEditableScore(runType, it) }
if (applicableScores.isEmpty()) {
this.sendRedMessage("No applicable scores found for $targetPlayer")
return@async
}
/*
Decked Out 2 config:
- Dungeon difficulty [easy] [medium] [hard] [deadly] [deepfrost]
AKA:
/tellraw @a ["",{"text":"Decked Out 2 config:","bold":true,"italic":true,"color":"gold"},{"text":"\n- "},{"text":"Dungeon difficulty","bold":true},{"text":" [easy]","color":"aqua"},{"text":" [","color":"light_purple"},{"text":"medium","color":"light_purple","clickEvent":{"action":"run_command","value":"do config set do2.config.dungeonDifficulty 1"}},{"text":"] [hard] [deadly] [deepfrost]","color":"light_purple"}]
Next we convert this to MiniMessage:
*/

this.sendMessage(mm.deserialize("<gold><bold>Decked Out 2 config for ${targetPlayer}:</bold></gold>"))
applicableScores.mapNotNull { getPlayerScore(runType, it) }.sortedBy { it.key }.forEach { score ->

val opts = score.config.values?.map { possibleValue ->
if (possibleValue.key == score.value?.toInt()?.toString()) {
return@map Component.text("[${possibleValue.value}]")
.hoverEvent(HoverEvent.showText(Component.text("Current value is ${possibleValue.value}")))
.color(NamedTextColor.AQUA)
} else {
val command = ClickEvent.runCommand("/do config set ${score.key} ${possibleValue.key}")
return@map Component.text("[${possibleValue.value}]")
.clickEvent(command)
.hoverEvent(HoverEvent.showText(Component.text("Click to set to ${possibleValue.value}")))
.color(NamedTextColor.LIGHT_PURPLE)
}
}

val scoreTitleWithAnnotation = "<hover:show_text:'${score.config.description}'>${score.config.displayText ?: score.key}</hover>"
var parsed = mm.deserialize("- <white>$scoreTitleWithAnnotation</white>:")
opts?.forEach {
parsed = parsed.append(mm.deserialize(" ")).append(it)
}

this.sendMessage(parsed)
}
}
}

data class PlayerScore(
val key: String,
val config: BrillianceScoreboardDescription,
val value: BigDecimal?,
)

private fun isEditableScore(runType: RunTypes, score: Score): Boolean {
val key = score.key
if (key == null || !key.startsWith("${runType.runType}-")) {
return false
}

return editableConfigs.contains(key) || scoreboardMap[score.scoreKeyWithoutRunPrefix(runType)]?.values?.isNotEmpty() == true
}

private fun getPlayerScore(runType: RunTypes, score: Score): PlayerScore? {
plugin.logger.info("getPlayerScore: $score")
return scoreboardMap[score.scoreKeyWithoutRunPrefix(runType)]?.let {
return PlayerScore(score.key!!, it, score.value)
}
}

private fun Score.scoreKeyWithoutRunPrefix(runType: RunTypes) = key?.removePrefix("${runType.runType}-")

fun setConfig(source: CommandSender, key: String, value: Int) {
val mm = MiniMessage.miniMessage()

plugin.async(source) {
val scores = scoreApi.scoresGet(player = source.name).results!!

val score = scores.find { it.key == key }
if (score == null) {
source.sendRedMessage("No score found for $key")
return@async
}

// val updatedScore = scoreApi.scoresPost(
// score.id,
// Score(
// key = key,
// value = value.toString(),
// player = source.name,
// )
// )

source.sendMessage(mm.deserialize("[not really] Updated score for $key to $value"))
}
}

private val editableConfigs = listOf(
"do2.inventory.shards.competitive"
)

fun getConfigsForPlayer(source: CommandSender, playerName: String) {
val mm = MiniMessage.miniMessage()
plugin.async(source) {
val database = MongoDBManager.getDatabase()
val playerStatsCollection = database.getCollection("playerStats", MongoPlayerStats::class.java)

/*
{
_id: ObjectId('674f5e20dc8b1753eb673c6b'),
player: 'InaByt',
key: 'do2.inventory.shards.competitive',
value: 0,
createdAt: ISODate('2024-12-03T19:38:08.433Z'),
updatedAt: ISODate('2024-12-14T18:10:27.760Z'),
__v: 0
},
*/

val scores = playerStatsCollection.find(
Filters.and(
eq("player", playerName),
Filters.or(
editableConfigs.map {
eq("key", it)
}
)
),
).toList()

if (scores.isEmpty()) {
source.sendRedMessage("Unable to find any configs for player $playerName")
return@async
}

val message = """
Scores for <orange>${playerName}</orange>:
""".trimIndent()
scores.map { "- ${it.player} = ${it.stats.total}" }

source.sendMessage(mm.deserialize(message))
}
}
}
19 changes: 19 additions & 0 deletions src/main/kotlin/org/trackedout/citadel/config/BrillianceData.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package org.trackedout.citadel.config

import kotlinx.serialization.json.Json
import org.trackedout.data.BrillianceScoreboardDescription

val json = Json { ignoreUnknownKeys = true }
typealias ScoreboardMap = Map<String, BrillianceScoreboardDescription>

val scoreboardMap: ScoreboardMap by lazy {
loadScoreboardMap() ?: throw IllegalStateException("Failed to load the scoreboard map")
}

private fun loadScoreboardMap(): ScoreboardMap? {
// Load the JSON file content
return object {}.javaClass.getResource("/items_json/scoreboards.json")?.readText()?.let {
// Deserialize the JSON content into the ScoreboardMap type
json.decodeFromString(it)
}
}

0 comments on commit c71e3e0

Please sign in to comment.