Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions serverdata/skill/xp_limits.sdb
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ object ServerData {
* Skill / Collection
*/
val skills by SoftDataLoaderDelegate(::SkillLoader)
val xpLimits by SoftDataLoaderDelegate(::XpLimitLoader)

/*
* Player
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>. *
***********************************************************************************/
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<String, Int> = 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()
}
}
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>. *
***********************************************************************************/
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
}
}
9 changes: 9 additions & 0 deletions src/test/java/com/projectswg/holocore/headless/admin.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading