diff --git a/src/main/java/com/projectswg/holocore/intents/gameplay/player/character/TippingIntents.kt b/src/main/java/com/projectswg/holocore/intents/gameplay/player/character/TippingIntents.kt
new file mode 100644
index 000000000..020f9917c
--- /dev/null
+++ b/src/main/java/com/projectswg/holocore/intents/gameplay/player/character/TippingIntents.kt
@@ -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 . *
+ ***********************************************************************************/
+@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()
+ }
+}
diff --git a/src/main/java/com/projectswg/holocore/resources/support/global/commands/callbacks/CmdTip.kt b/src/main/java/com/projectswg/holocore/resources/support/global/commands/callbacks/CmdTip.kt
new file mode 100644
index 000000000..a8333e982
--- /dev/null
+++ b/src/main/java/com/projectswg/holocore/resources/support/global/commands/callbacks/CmdTip.kt
@@ -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 . *
+ ***********************************************************************************/
+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
+ }
+ }
+}
diff --git a/src/main/java/com/projectswg/holocore/services/gameplay/player/PlayerManager.kt b/src/main/java/com/projectswg/holocore/services/gameplay/player/PlayerManager.kt
index 2214f07d9..69a033944 100644
--- a/src/main/java/com/projectswg/holocore/services/gameplay/player/PlayerManager.kt
+++ b/src/main/java/com/projectswg/holocore/services/gameplay/player/PlayerManager.kt
@@ -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()
diff --git a/src/main/java/com/projectswg/holocore/services/gameplay/player/character/TippingService.kt b/src/main/java/com/projectswg/holocore/services/gameplay/player/character/TippingService.kt
new file mode 100644
index 000000000..94a77394a
--- /dev/null
+++ b/src/main/java/com/projectswg/holocore/services/gameplay/player/character/TippingService.kt
@@ -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 . *
+ ***********************************************************************************/
+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()
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/projectswg/holocore/services/support/global/commands/CommandExecutionService.java b/src/main/java/com/projectswg/holocore/services/support/global/commands/CommandExecutionService.java
index b5bc1dbc2..6fc3416ff 100644
--- a/src/main/java/com/projectswg/holocore/services/support/global/commands/CommandExecutionService.java
+++ b/src/main/java/com/projectswg/holocore/services/support/global/commands/CommandExecutionService.java
@@ -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();
diff --git a/src/test/java/com/projectswg/holocore/services/gameplay/player/TipCreditsTest.kt b/src/test/java/com/projectswg/holocore/services/gameplay/player/TipCreditsTest.kt
new file mode 100644
index 000000000..621792a19
--- /dev/null
+++ b/src/test/java/com/projectswg/holocore/services/gameplay/player/TipCreditsTest.kt
@@ -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 . *
+ ***********************************************************************************/
+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 {
+ 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
+}