diff --git a/serverdata/skill/xp_limits.sdb b/serverdata/skill/xp_limits.sdb new file mode 100644 index 000000000..e868f7e21 --- /dev/null +++ b/serverdata/skill/xp_limits.sdb @@ -0,0 +1,51 @@ +name limit + +apprenticeship 1240 +crafting_bio_engineer_creature 20000 +bio_engineer_dna_harvesting 20000 +bountyhunter 10000 +camp 2000 +combat_general 170000 +combat_meleespecialize_onehandlightsaber 3000 +combat_meleespecialize_twohandlightsaber 200000 +combat_meleespecialize_polearmlightsaber 200000 +jedi_general 90000 +combat_meleespecialize_onehand 10000 +combat_meleespecialize_polearm 10000 +combat_meleespecialize_twohand 10000 +combat_meleespecialize_unarmed 10000 +combat_rangedspecialize_carbine 200000 +combat_rangedspecialize_pistol 200000 +combat_rangedspecialize_rifle 200000 +crafting_clothing_armor 56000 +crafting_clothing_general 56000 +crafting_droid_general 56000 +crafting_food_general 56000 +crafting_general 1000 +crafting_medicine_general 1000 +crafting_spice 45000 +crafting_structure_general 140000 +crafting_weapons_general 56000 +creaturehandler 250000 +dance 2000 +entertainer_healing 2000 +entertainer 2000 +imagedesigner 2000 +jedi 2000 +medical 2000 +merchant 10000 +music 2000 +resource_harvesting_inorganic 1000 +scout 2000 +slicing 30000 +squadleader 500000 +trapping 2000 +combat_rangedspecialize_heavy 300000 +political 20000 +force_rank_xp 625000 +fs_combat 5100000 +fs_crafting 5100000 +fs_senses 5100000 +fs_reflex 5100000 +shipwright 60000 +space_combat_general 10000 diff --git a/src/main/java/com/projectswg/holocore/resources/support/data/server_info/loader/ServerData.kt b/src/main/java/com/projectswg/holocore/resources/support/data/server_info/loader/ServerData.kt index 2421c9ae6..df2188381 100644 --- a/src/main/java/com/projectswg/holocore/resources/support/data/server_info/loader/ServerData.kt +++ b/src/main/java/com/projectswg/holocore/resources/support/data/server_info/loader/ServerData.kt @@ -52,6 +52,7 @@ object ServerData { * Skill / Collection */ val skills by SoftDataLoaderDelegate(::SkillLoader) + val xpLimits by SoftDataLoaderDelegate(::XpLimitLoader) /* * Player diff --git a/src/main/java/com/projectswg/holocore/resources/support/data/server_info/loader/XpLimitLoader.kt b/src/main/java/com/projectswg/holocore/resources/support/data/server_info/loader/XpLimitLoader.kt new file mode 100644 index 000000000..535441dc0 --- /dev/null +++ b/src/main/java/com/projectswg/holocore/resources/support/data/server_info/loader/XpLimitLoader.kt @@ -0,0 +1,45 @@ +/*********************************************************************************** + * Copyright (c) 2026 /// Project SWG /// www.projectswg.com * + * * + * ProjectSWG is an emulation project for Star Wars Galaxies founded on * + * July 7th, 2011 after SOE announced the official shutdown of Star Wars Galaxies. * + * Our goal is to create one or more emulators which will provide servers for * + * players to continue playing a game similar to the one they used to play. * + * * + * This file is part of Holocore. * + * * + * --------------------------------------------------------------------------------* + * * + * Holocore is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as * + * published by the Free Software Foundation, either version 3 of the * + * License, or (at your option) any later version. * + * * + * Holocore is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with Holocore. If not, see . * + ***********************************************************************************/ +package com.projectswg.holocore.resources.support.data.server_info.loader + +import com.projectswg.holocore.resources.support.data.server_info.SdbLoader +import java.io.File +import java.io.IOException + +class XpLimitLoader internal constructor() : DataLoader() { + private val limits: MutableMap = HashMap() + + fun getLimit(xpType: String): Int? = limits[xpType] + + @Throws(IOException::class) + override fun load() { + SdbLoader.load(File("serverdata/skill/xp_limits.sdb")).use { set -> + while (set.next()) { + limits[set.getText("name")] = set.getInt("limit").toInt() + } + } + } +} diff --git a/src/main/java/com/projectswg/holocore/services/gameplay/player/experience/ExperiencePointService.java b/src/main/java/com/projectswg/holocore/services/gameplay/player/experience/ExperiencePointService.java deleted file mode 100644 index 2d98b07f4..000000000 --- a/src/main/java/com/projectswg/holocore/services/gameplay/player/experience/ExperiencePointService.java +++ /dev/null @@ -1,101 +0,0 @@ -/*********************************************************************************** - * Copyright (c) 2024 /// 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.experience; - -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.network.packets.swg.zone.chat.ChatSystemMessage; -import com.projectswg.common.network.packets.swg.zone.object_controller.ShowFlyText; -import com.projectswg.common.network.packets.swg.zone.object_controller.ShowFlyText.Scale; -import com.projectswg.holocore.intents.gameplay.player.experience.ExperienceIntent; -import com.projectswg.holocore.intents.support.global.chat.SystemMessageIntent; -import com.projectswg.holocore.resources.support.color.SWGColor; -import com.projectswg.holocore.resources.support.data.server_info.StandardLog; -import com.projectswg.holocore.resources.support.data.server_info.mongodb.PswgDatabase; -import com.projectswg.holocore.resources.support.objects.swg.SWGObject; -import com.projectswg.holocore.resources.support.objects.swg.creature.CreatureObject; -import com.projectswg.holocore.resources.support.objects.swg.player.PlayerObject; -import me.joshlarson.jlcommon.control.IntentHandler; -import me.joshlarson.jlcommon.control.Service; - -import java.util.Objects; - -public class ExperiencePointService extends Service { - - private final double xpMultiplier; - - public ExperiencePointService() { - xpMultiplier = PswgDatabase.INSTANCE.getConfig().getDouble(this, "xpMultiplier", 1); - } - - @IntentHandler - private void handleExperienceIntent(ExperienceIntent ei) { - CreatureObject creatureObject = ei.getCreatureObject(); - PlayerObject playerObject = creatureObject.getPlayerObject(); - - if (playerObject != null) { - String xpType = ei.getXpType(); - int experienceGained = ei.getExperienceGained(); - SWGObject flytextTarget = ei.getFlytextTarget(); - boolean xpMultiplied = ei.isXpMultiplied(); - - if (xpMultiplied) { - experienceGained *= xpMultiplier; - } - - awardExperience(creatureObject, flytextTarget, playerObject, xpType, experienceGained); - } - } - - private void awardExperience(CreatureObject creatureObject, SWGObject flytextTarget, PlayerObject playerObject, String xpType, int xpGained) { - int currentXp = playerObject.getExperiencePoints(xpType); - int newXpTotal = currentXp + xpGained; - - playerObject.setExperiencePoints(xpType, newXpTotal); - StandardLog.onPlayerTrace(this, creatureObject, "gained %d %s XP", xpGained, xpType); - - if (!Objects.equals("combat_general", xpType)) { - showFlytext(creatureObject, flytextTarget, xpGained); - showSystemMessage(creatureObject, xpType); - } - } - - private void showSystemMessage(CreatureObject creatureObject, String xpType) { - // TODO display different messages with inspiration bonus and/or group bonus - StringId xpTypeDisplayName = new StringId("exp_n", xpType); - ProsePackage message = new ProsePackage(new StringId("base_player", "prose_grant_xp"), "TO", xpTypeDisplayName); - SystemMessageIntent.Companion.broadcastPersonal(creatureObject.getOwner(), message, ChatSystemMessage.SystemChatType.CHAT_BOX); - } - - private void showFlytext(CreatureObject creatureObject, SWGObject flytextTarget, int xpGained) { - OutOfBandPackage message = new OutOfBandPackage(new ProsePackage(new StringId("base_player", "prose_flytext_xp"), "DI", xpGained)); - ShowFlyText packet = new ShowFlyText(flytextTarget.getObjectId(), message, Scale.MEDIUM, SWGColor.Violets.INSTANCE.getMagenta()); - creatureObject.sendSelf(packet); - } - -} diff --git a/src/main/java/com/projectswg/holocore/services/gameplay/player/experience/ExperiencePointService.kt b/src/main/java/com/projectswg/holocore/services/gameplay/player/experience/ExperiencePointService.kt new file mode 100644 index 000000000..2d847b1c1 --- /dev/null +++ b/src/main/java/com/projectswg/holocore/services/gameplay/player/experience/ExperiencePointService.kt @@ -0,0 +1,123 @@ +/*********************************************************************************** + * Copyright (c) 2024 /// 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.experience + +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.network.packets.swg.zone.chat.ChatSystemMessage +import com.projectswg.common.network.packets.swg.zone.object_controller.ShowFlyText +import com.projectswg.holocore.intents.gameplay.player.experience.ExperienceIntent +import com.projectswg.holocore.intents.support.global.chat.SystemMessageIntent +import com.projectswg.holocore.resources.support.color.SWGColor +import com.projectswg.holocore.resources.support.data.server_info.StandardLog +import com.projectswg.holocore.resources.support.data.server_info.loader.ServerData +import com.projectswg.holocore.resources.support.data.server_info.mongodb.PswgDatabase +import com.projectswg.holocore.resources.support.objects.swg.SWGObject +import com.projectswg.holocore.resources.support.objects.swg.creature.CreatureObject +import com.projectswg.holocore.resources.support.objects.swg.player.PlayerObject +import me.joshlarson.jlcommon.control.IntentHandler +import me.joshlarson.jlcommon.control.Service + +class ExperiencePointService : Service() { + + private val xpMultiplier = PswgDatabase.config.getDouble(this, "xpMultiplier", 1.0) + + @IntentHandler + private fun handleExperienceIntent(ei: ExperienceIntent) { + val creatureObject = ei.creatureObject ?: return + val playerObject = creatureObject.playerObject ?: return + val xpType = ei.xpType ?: return + var experienceGained = ei.experienceGained + + if (ei.isXpMultiplied) { + experienceGained = (experienceGained * xpMultiplier).toInt() + } + + awardExperience(creatureObject, ei.flytextTarget, playerObject, xpType, experienceGained) + } + + private fun awardExperience(creatureObject: CreatureObject, flytextTarget: SWGObject?, playerObject: PlayerObject, xpType: String, xpGained: Int) { + val currentXp = playerObject.getExperiencePoints(xpType) + val xpLimit = getXpLimit(creatureObject, xpType) + + if (xpGained > 0 && currentXp >= xpLimit) { + StandardLog.onPlayerTrace(this, creatureObject, "gained no %s XP, already at the limit of %d", xpType, xpLimit) + return + } + + val newXpTotal = minOf(currentXp + xpGained, xpLimit) + val actualXpGained = newXpTotal - currentXp + + playerObject.setExperiencePoints(xpType, newXpTotal) + StandardLog.onPlayerTrace(this, creatureObject, "gained %d %s XP", actualXpGained, xpType) + + if (newXpTotal == xpLimit && xpGained > 0) { + StandardLog.onPlayerTrace(this, creatureObject, "reached the %s XP limit of %d", xpType, xpLimit) + } + + if (xpType != "combat_general") { + showFlytext(creatureObject, flytextTarget, actualXpGained) + showSystemMessage(creatureObject, xpType) + } + } + + /** + * The limit is the largest cap among the skills the player has for this XP type. Players without any such skill are + * held to the default limit from the client datatable, or [UNKNOWN_XP_TYPE_LIMIT] if the XP type has no default. + */ + private fun getXpLimit(creatureObject: CreatureObject, xpType: String): Int { + val skillLimit = creatureObject.skills + .mapNotNull { ServerData.skills.getSkillByName(it) } + .filter { it.xpType == xpType && it.xpCap > 0 } + .maxOfOrNull { it.xpCap } + + return skillLimit ?: ServerData.xpLimits.getLimit(xpType) ?: UNKNOWN_XP_TYPE_LIMIT + } + + private fun showSystemMessage(creatureObject: CreatureObject, xpType: String) { + // TODO display different messages with inspiration bonus and/or group bonus + val owner = creatureObject.owner ?: return + val xpTypeDisplayName = StringId("exp_n", xpType) + val message = ProsePackage(StringId("base_player", "prose_grant_xp"), "TO", xpTypeDisplayName) + SystemMessageIntent.broadcastPersonal(owner, message, ChatSystemMessage.SystemChatType.CHAT_BOX) + } + + private fun showFlytext(creatureObject: CreatureObject, flytextTarget: SWGObject?, xpGained: Int) { + if (flytextTarget == null) { + return + } + + val message = OutOfBandPackage(ProsePackage(StringId("base_player", "prose_flytext_xp"), "DI", xpGained)) + val packet = ShowFlyText(flytextTarget.objectId, message, ShowFlyText.Scale.MEDIUM, SWGColor.Violets.magenta) + creatureObject.sendSelf(packet) + } + + private companion object { + private const val UNKNOWN_XP_TYPE_LIMIT = 2000 + } +} diff --git a/src/test/java/com/projectswg/holocore/headless/admin.kt b/src/test/java/com/projectswg/holocore/headless/admin.kt index bdc70cf4f..02e29c937 100644 --- a/src/test/java/com/projectswg/holocore/headless/admin.kt +++ b/src/test/java/com/projectswg/holocore/headless/admin.kt @@ -51,6 +51,15 @@ fun ZonedInCharacter.adminGrantSkill(skill: String) { player.waitForNextObjectDelta(player.creatureObject.objectId, 4, 14, 50, TimeUnit.MILLISECONDS) ?: java.lang.IllegalStateException("No known packet received") } +/** + * Admin command /setExperience + * @param xpType the type of XP to award + * @param xpGained the amount of XP to award + */ +fun ZonedInCharacter.adminSetExperience(xpType: String, xpGained: Int) { + sendCommand("setExperience", player.creatureObject, "$xpType $xpGained") +} + /** * Admin command /teleport * @param target The object to teleport. If null, the player executing the command will be teleported. diff --git a/src/test/java/com/projectswg/holocore/services/gameplay/player/experience/XpLimitTest.kt b/src/test/java/com/projectswg/holocore/services/gameplay/player/experience/XpLimitTest.kt new file mode 100644 index 000000000..c016e7c3e --- /dev/null +++ b/src/test/java/com/projectswg/holocore/services/gameplay/player/experience/XpLimitTest.kt @@ -0,0 +1,72 @@ +/*********************************************************************************** + * Copyright (c) 2026 /// Project SWG /// www.projectswg.com * + * * + * ProjectSWG is an emulation project for Star Wars Galaxies founded on * + * July 7th, 2011 after SOE announced the official shutdown of Star Wars Galaxies. * + * Our goal is to create one or more emulators which will provide servers for * + * players to continue playing a game similar to the one they used to play. * + * * + * 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.experience + +import com.projectswg.holocore.headless.* +import com.projectswg.holocore.resources.support.global.player.AccessLevel +import com.projectswg.holocore.test.runners.AcceptanceTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class XpLimitTest : AcceptanceTest() { + + @Test + fun defaultLimitCapsXp() { + val character = createCharacter() + + character.adminSetExperience("trapping", 5000) + + character.waitForExperiencePoints("trapping") + assertEquals(2000, character.getXP("trapping")) + } + + @Test + fun skillBoxRaisesLimit() { + val character = createCharacter() + character.adminGrantSkill("outdoors_scout_tools_01") + + character.adminSetExperience("trapping", 5000) + + character.waitForExperiencePoints("trapping") + assertEquals(5000, character.getXP("trapping")) + } + + @Test + fun noXpGainedWhenAtLimit() { + val character = createCharacter() + character.adminSetExperience("trapping", 2000) + character.waitForExperiencePoints("trapping") + + character.adminSetExperience("trapping", 1000) + + assertEquals(2000, character.getXP("trapping")) + } + + private fun createCharacter(): ZonedInCharacter { + val user = generateUser(accessLevel = AccessLevel.DEV) + return HeadlessSWGClient.createZonedInCharacter(user.username, user.password, "adminchar") + } +} diff --git a/src/utility/java/com/projectswg/utility/ClientdataConvertAll.java b/src/utility/java/com/projectswg/utility/ClientdataConvertAll.java index 6ccc78872..b032ee741 100644 --- a/src/utility/java/com/projectswg/utility/ClientdataConvertAll.java +++ b/src/utility/java/com/projectswg/utility/ClientdataConvertAll.java @@ -29,6 +29,7 @@ public static void main(String [] args) throws IOException { Converters.QUESTTASK.load(); Converters.SCHEMATIC_GROUP.load(); Converters.FACTION_RANK.load(); + Converters.XP_LIMITS.load(); } } diff --git a/src/utility/java/com/projectswg/utility/clientdata/Converters.java b/src/utility/java/com/projectswg/utility/clientdata/Converters.java index 933f9f864..c7b889cc1 100644 --- a/src/utility/java/com/projectswg/utility/clientdata/Converters.java +++ b/src/utility/java/com/projectswg/utility/clientdata/Converters.java @@ -23,6 +23,7 @@ public enum Converters { QUESTTASK (ConvertQuestTasks::new), SCHEMATIC_GROUP (() -> new ConvertDatatable("datatables/crafting/schematic_group.iff", "serverdata/crafting/schematic_group.sdb", true)), FACTION_RANK (() -> new ConvertDatatable("datatables/faction/rank.iff", "serverdata/faction/rank.sdb", true)), + XP_LIMITS (() -> new ConvertDatatable("datatables/skill/xp_limits.iff", "serverdata/skill/xp_limits.sdb", true)), STRINGS (ConvertStrings::new), TERRAINS (ConvertTerrain::new);