Added support for the /tip command

This commit is contained in:
Ziggy
2023-11-28 21:36:30 +01:00
parent 5575f3da20
commit fa8a6d2d91
6 changed files with 481 additions and 1 deletions
@@ -0,0 +1,43 @@
/***********************************************************************************
* Copyright (c) 2023 /// 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/>. *
***********************************************************************************/
@file:Suppress("NOTHING_TO_INLINE")
package com.projectswg.holocore.intents.gameplay.player.character
import com.projectswg.holocore.resources.support.global.player.Player
import me.joshlarson.jlcommon.control.Intent
data class CashTipIntent(val sender: Player, val receiver: Player, val amount: Int): Intent() {
companion object {
@JvmStatic inline fun broadcast(sender: Player, receiver: Player, amount: Int) = CashTipIntent(sender, receiver, amount).broadcast()
}
}
data class BankTipIntent(val sender: Player, val receiver: Player, val amount: Int): Intent() {
companion object {
@JvmStatic inline fun broadcast(sender: Player, receiver: Player, amount: Int) = BankTipIntent(sender, receiver, amount).broadcast()
}
}
@@ -0,0 +1,90 @@
/***********************************************************************************
* Copyright (c) 2023 /// 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.global.commands.callbacks
import com.projectswg.common.data.encodables.oob.ProsePackage
import com.projectswg.common.data.encodables.oob.StringId
import com.projectswg.holocore.intents.gameplay.player.character.BankTipIntent
import com.projectswg.holocore.intents.gameplay.player.character.CashTipIntent
import com.projectswg.holocore.intents.support.global.chat.SystemMessageIntent
import com.projectswg.holocore.resources.support.global.commands.ICmdCallback
import com.projectswg.holocore.resources.support.global.player.Player
import com.projectswg.holocore.resources.support.objects.swg.SWGObject
import com.projectswg.holocore.resources.support.objects.swg.creature.CreatureObject
private enum class TipType {
CASH,
BANK
}
class CmdTip : ICmdCallback {
override fun execute(player: Player, target: SWGObject?, args: String) {
val tipType = tipType(args)
val amount = args.replace(" bank", "").toIntOrNull()
if (amount == null) {
SystemMessageIntent.broadcastPersonal(player, ProsePackage(StringId("base_player", "tip_syntax")))
return
}
if (amount <= 0) {
SystemMessageIntent.broadcastPersonal(player, ProsePackage(StringId("base_player", "prose_tip_invalid_amt"), "DI", amount))
return
}
if (target !is CreatureObject) {
SystemMessageIntent.broadcastPersonal(player, "You cannot tip a non-player object")
return
}
val targetPlayer = target.owner
if (targetPlayer == null) {
SystemMessageIntent.broadcastPersonal(player, "You cannot tip a non-player object")
return
}
if (target == player.creatureObject) {
SystemMessageIntent.broadcastPersonal(player, "You cannot tip yourself")
return
}
if (tipType == TipType.CASH) {
CashTipIntent.broadcast(player, targetPlayer, amount)
} else if (tipType == TipType.BANK) {
BankTipIntent.broadcast(player, targetPlayer, amount)
}
}
private fun tipType(args: String): TipType {
return if (args.endsWith(" bank")) {
TipType.BANK
} else {
TipType.CASH
}
}
}
@@ -28,6 +28,7 @@ package com.projectswg.holocore.services.gameplay.player
import com.projectswg.holocore.services.gameplay.player.badge.BadgeManager
import com.projectswg.holocore.services.gameplay.player.character.PlayerCharacterSheetService
import com.projectswg.holocore.services.gameplay.player.character.TippingService
import com.projectswg.holocore.services.gameplay.player.experience.ExperienceManager
import com.projectswg.holocore.services.gameplay.player.group.GroupManager
import com.projectswg.holocore.services.gameplay.player.guild.GuildService
@@ -35,5 +36,5 @@ import com.projectswg.holocore.services.gameplay.player.quest.QuestService
import me.joshlarson.jlcommon.control.Manager
import me.joshlarson.jlcommon.control.ManagerStructure
@ManagerStructure(children = [BadgeManager::class, ExperienceManager::class, GroupManager::class, GuildService::class, QuestService::class, PlayerCharacterSheetService::class])
@ManagerStructure(children = [BadgeManager::class, ExperienceManager::class, GroupManager::class, GuildService::class, QuestService::class, PlayerCharacterSheetService::class, TippingService::class])
class PlayerManager : Manager()
@@ -0,0 +1,127 @@
/***********************************************************************************
* Copyright (c) 2023 /// 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.services.gameplay.player.character
import com.projectswg.common.data.encodables.oob.OutOfBandPackage
import com.projectswg.common.data.encodables.oob.ProsePackage
import com.projectswg.common.data.encodables.oob.StringId
import com.projectswg.common.data.encodables.player.Mail
import com.projectswg.holocore.ProjectSWG
import com.projectswg.holocore.intents.gameplay.player.character.BankTipIntent
import com.projectswg.holocore.intents.gameplay.player.character.CashTipIntent
import com.projectswg.holocore.intents.support.global.chat.PersistentMessageIntent
import com.projectswg.holocore.intents.support.global.chat.SystemMessageIntent
import com.projectswg.holocore.resources.support.data.server_info.StandardLog
import com.projectswg.holocore.resources.support.global.player.Player
import com.projectswg.holocore.resources.support.global.zone.sui.SuiMessageBox
import com.projectswg.holocore.resources.support.objects.swg.creature.CreatureObject
import me.joshlarson.jlcommon.control.IntentHandler
import me.joshlarson.jlcommon.control.Service
class TippingService : Service() {
@IntentHandler
private fun handleCashTip(intent: CashTipIntent) {
val amount = intent.amount
val player = intent.sender
val targetPlayer = intent.receiver
val target = targetPlayer.creatureObject
if (amount > player.creatureObject.cashBalance) {
SystemMessageIntent.broadcastPersonal(player, ProsePackage(StringId("base_player", "prose_tip_nsf_cash"), "DI", amount, "TT", target.objectName))
return
}
transferCash(player, amount, targetPlayer)
}
@IntentHandler
private fun handleBankTip(intent: BankTipIntent) {
val amount = intent.amount
val player = intent.sender
val targetPlayer = intent.receiver
val target = targetPlayer.creatureObject
if (amount > player.creatureObject.bankBalance) {
SystemMessageIntent.broadcastPersonal(player, ProsePackage(StringId("base_player", "prose_tip_nsf_bank"), "DI", amount, "TT", target.objectName))
return
}
val amountWithSurcharge = (amount * 1.05).toInt() // System applies a 5% surcharge to the bank tip
if (amountWithSurcharge > player.creatureObject.bankBalance) {
SystemMessageIntent.broadcastPersonal(player, ProsePackage(StringId("base_player", "prose_tip_nsf_wire"), "DI", amountWithSurcharge, "TT", target.objectName))
return
}
val suiMessageBox = SuiMessageBox("@base_player:tip_wire_title", "@base_player:tip_wire_prompt")
suiMessageBox.addOkButtonCallback("tip") { _, _ ->
wire(player, target, amount, amountWithSurcharge)
}
suiMessageBox.display(player)
}
private fun transferCash(player: Player, amount: Int, targetPlayer: Player) {
val target = targetPlayer.creatureObject
player.creatureObject.setCashBalance((player.creatureObject.cashBalance - amount).toLong())
target.setCashBalance((target.cashBalance + amount).toLong())
StandardLog.onPlayerEvent(this, player, "tipped %d credits to %s using cash", amount, target)
sendSystemMessages(player, amount, target, targetPlayer)
}
private fun sendSystemMessages(player: Player, amount: Int, target: CreatureObject, targetPlayer: Player) {
SystemMessageIntent.broadcastPersonal(
player, ProsePackage(StringId("base_player", "prose_tip_pass_self"), "DI", amount, "TT", target.objectName)
)
SystemMessageIntent.broadcastPersonal(
targetPlayer, ProsePackage(StringId("base_player", "prose_tip_pass_target"), "TT", player.creatureObject.objectName, "DI", amount)
)
}
private fun wire(player: Player, target: CreatureObject, amount: Int, amountWithSurcharge: Int) {
player.creatureObject.setBankBalance((player.creatureObject.bankBalance - amountWithSurcharge).toLong())
target.setBankBalance((target.bankBalance + amount).toLong())
StandardLog.onPlayerEvent(this, player, "tipped %d credits to %s using wire transfer", amount, target)
sendMails(target, amount, player)
}
private fun sendMails(target: CreatureObject, amount: Int, player: Player) {
val targetMail = Mail("@money/acct_n:bank", "@base_player:wire_mail_subject", "", target.objectId)
targetMail.outOfBandPackage = OutOfBandPackage(
ProsePackage(StringId("base_player", "prose_wire_mail_target"), "DI", amount, "TO", player.creatureObject.objectName),
)
PersistentMessageIntent(target, targetMail, ProjectSWG.galaxy.name).broadcast()
val selfMail = Mail(target.objectName, "@base_player:wire_mail_subject", "", player.creatureObject.objectId)
selfMail.outOfBandPackage = OutOfBandPackage(
ProsePackage(StringId("base_player", "prose_wire_mail_self"), "TO", target.objectName, "DI", amount),
)
PersistentMessageIntent(player.creatureObject, selfMail, ProjectSWG.galaxy.name).broadcast()
}
}
@@ -123,6 +123,7 @@ public class CommandExecutionService extends Service {
registerScriptCallback("knockdownRecovery", KnockdownRecoveryCmdCallback::new);
registerScriptCallback("burstRun", BurstRunCmdCallback::new);
registerScriptCallback("cmdMeditate", CmdMeditate::new);
registerScriptCallback("cmdTip", CmdTip::new);
addAdminScripts();
addChatScripts();
@@ -0,0 +1,218 @@
/***********************************************************************************
* Copyright (c) 2023 /// 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.services.gameplay.player
import com.projectswg.common.data.location.Location
import com.projectswg.holocore.headless.*
import com.projectswg.holocore.resources.support.data.server_info.loader.ServerData
import com.projectswg.holocore.resources.support.data.server_info.loader.npc.NpcStaticSpawnLoader
import com.projectswg.holocore.resources.support.npc.spawn.NPCCreator
import com.projectswg.holocore.resources.support.npc.spawn.SimpleSpawnInfo
import com.projectswg.holocore.resources.support.npc.spawn.Spawner
import com.projectswg.holocore.resources.support.objects.ObjectCreator
import com.projectswg.holocore.resources.support.objects.swg.creature.CreatureDifficulty
import com.projectswg.holocore.resources.support.objects.swg.custom.AIObject
import com.projectswg.holocore.services.gameplay.player.character.TippingService
import com.projectswg.holocore.services.gameplay.player.experience.skills.SkillService
import com.projectswg.holocore.services.support.global.chat.ChatMailService
import com.projectswg.holocore.services.support.global.chat.ChatSystemService
import com.projectswg.holocore.services.support.global.commands.CommandExecutionService
import com.projectswg.holocore.services.support.global.commands.CommandQueueService
import com.projectswg.holocore.services.support.global.zone.LoginService
import com.projectswg.holocore.services.support.global.zone.ZoneService
import com.projectswg.holocore.services.support.global.zone.creation.CharacterCreationService
import com.projectswg.holocore.services.support.global.zone.sui.SuiService
import com.projectswg.holocore.test.runners.TestRunnerSimulatedWorld
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
class TipCreditsTest : TestRunnerSimulatedWorld() {
private val memoryUserDatabase = MemoryUserDatabase()
@BeforeEach
fun setUp() {
registerService(LoginService(memoryUserDatabase))
registerService(ZoneService())
registerService(CharacterCreationService())
registerService(SkillService())
registerService(CommandQueueService(5))
registerService(CommandExecutionService())
registerService(ChatSystemService())
registerService(ChatMailService())
registerService(SuiService())
registerService(TippingService())
}
@AfterEach
fun tearDown() {
memoryUserDatabase.clear()
}
@Test
fun negativeAmount() {
val zonedInCharacter1 = createZonedInCharacter("Playerone", "Charone")
val zonedInCharacter2 = createZonedInCharacter("Playertwo", "Chartwo")
val tipAmount = -50
assertThrows(TipException::class.java) {
zonedInCharacter1.tipCash(zonedInCharacter2.player.creatureObject, tipAmount)
}
}
@Test
fun notEnoughMoneyForSurcharge() {
val zonedInCharacter1 = createZonedInCharacter("Playerone", "Charone")
val zonedInCharacter2 = createZonedInCharacter("Playertwo", "Chartwo")
val tipAmount = zonedInCharacter1.player.creatureObject.bankBalance // Sending all bank balance should never be possible, because of the 5% surcharge
assertThrows(TipException::class.java) {
zonedInCharacter1.tipBank(zonedInCharacter2.player.creatureObject, tipAmount)
}
}
@Test
fun npc() {
val zonedInCharacter1 = createZonedInCharacter("Playerone", "Charone")
val womprat = spawnNPCs("creature_womprat", zonedInCharacter1.player.creatureObject.location, 1).first()
assertThrows(TipException::class.java) {
zonedInCharacter1.tipCash(womprat, 1)
}
}
@Test
fun self() {
val zonedInCharacter1 = createZonedInCharacter("Playerone", "Charone")
assertThrows(TipException::class.java) {
zonedInCharacter1.tipCash(zonedInCharacter1.player.creatureObject, 1)
}
}
@Test
fun sufficientCash() {
val zonedInCharacter1 = createZonedInCharacter("Playerone", "Charone")
val zonedInCharacter2 = createZonedInCharacter("Playertwo", "Chartwo")
val char1Before = CreditsSnapshot(zonedInCharacter1)
val char2Before = CreditsSnapshot(zonedInCharacter2)
val tipAmount = 1
zonedInCharacter1.tipCash(zonedInCharacter2.player.creatureObject, tipAmount)
val char1After = CreditsSnapshot(zonedInCharacter1)
val char2After = CreditsSnapshot(zonedInCharacter2)
assertAll(
{ assertEquals(char1Before.cash - tipAmount, char1After.cash) },
{ assertEquals(char2Before.cash + tipAmount, char2After.cash) },
)
}
@Test
fun insufficientCash() {
val zonedInCharacter1 = createZonedInCharacter("Playerone", "Charone")
val zonedInCharacter2 = createZonedInCharacter("Playertwo", "Chartwo")
val tipAmount = zonedInCharacter1.player.creatureObject.cashBalance * 2
assertThrows(TipException::class.java) {
zonedInCharacter1.tipCash(zonedInCharacter2.player.creatureObject, tipAmount)
}
}
@Test
fun sufficientBank() {
val zonedInCharacter1 = createZonedInCharacter("Playerone", "Charone")
val zonedInCharacter2 = createZonedInCharacter("Playertwo", "Chartwo")
val char1Before = CreditsSnapshot(zonedInCharacter1)
val char2Before = CreditsSnapshot(zonedInCharacter2)
val tipAmount = 100
val suiWindow = zonedInCharacter1.tipBank(zonedInCharacter2.player.creatureObject, tipAmount)
suiWindow.clickOk()
zonedInCharacter1.waitForMail()
zonedInCharacter2.waitForMail()
val char1After = CreditsSnapshot(zonedInCharacter1)
val char2After = CreditsSnapshot(zonedInCharacter2)
assertAll(
{ assertEquals(char1Before.bank - (tipAmount * 1.05).toInt(), char1After.bank) },
{ assertEquals(char2Before.bank + tipAmount, char2After.bank) },
)
}
@Test
fun insufficientBank() {
val zonedInCharacter1 = createZonedInCharacter("Playerone", "Charone")
val zonedInCharacter2 = createZonedInCharacter("Playertwo", "Chartwo")
val char1Before = CreditsSnapshot(zonedInCharacter1)
val tipAmount = char1Before.bank * 2
assertThrows(TipException::class.java) {
zonedInCharacter1.tipBank(zonedInCharacter2.player.creatureObject, tipAmount)
}
}
private fun createZonedInCharacter(username: String, characterName: String): ZonedInCharacter {
val password = "password"
memoryUserDatabase.addUser(username, password)
return HeadlessSWGClient.createZonedInCharacter(username, password, characterName)
}
companion object {
@JvmStatic
@BeforeAll
fun setUpAll() {
ServerData.terrains // Eagerly load, so even the very first command is fast in the 'swimming' state check
}
}
private fun spawnNPCs(npcId: String, location: Location, amount: Int): Collection<AIObject> {
val egg = ObjectCreator.createObjectFromTemplate("object/tangible/ground_spawning/shared_patrol_spawner.iff")
egg.moveToContainer(null, location)
val spawnInfo = SimpleSpawnInfo.builder()
.withNpcId(npcId)
.withDifficulty(CreatureDifficulty.NORMAL)
.withMinLevel(1)
.withMaxLevel(1)
.withLocation(location)
.withAmount(amount)
.withSpawnerFlag(NpcStaticSpawnLoader.SpawnerFlag.ATTACKABLE)
.build()
return NPCCreator.createAllNPCs(Spawner(spawnInfo, egg))
}
}
private data class CreditsSnapshot(private val character: ZonedInCharacter) {
val cash = character.player.creatureObject.cashBalance
val bank = character.player.creatureObject.bankBalance
}