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
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/***********************************************************************************
* 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.intents.gameplay.items

import com.projectswg.holocore.resources.support.objects.swg.creature.CreatureObject
import com.projectswg.holocore.resources.support.objects.swg.tangible.TangibleObject
import me.joshlarson.jlcommon.control.Intent

data class RequestBioLinkIntent(val linker: CreatureObject, val item: TangibleObject) : Intent()
data class BioLinkNowIntent(val linker: CreatureObject, val item: TangibleObject) : Intent()
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,22 @@ class TransferItemCallback : ICmdCallback {
}

if (target is TangibleObject) {
if (target.isBioLinkRequired) {
val bioLinkedTo = target.bioLinkedTo

if (bioLinkedTo == null) {
SystemMessageIntent(player, "@base_player:not_linked").broadcast()
player.sendPacket(PlayMusicMessage(0, "sound/ui_dialog_warning.snd", 1, false))
return
}

if (bioLinkedTo != actor.objectId) {
SystemMessageIntent(player, "@container_error_message:container31").broadcast()
player.sendPacket(PlayMusicMessage(0, "sound/ui_dialog_warning.snd", 1, false))
return
}
}

val armorCategory: ArmorCategory? = target.armorCategory

if (armorCategory != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ object StaticItemCreator {
}
}
obj.armorCategory = info.armorCategory
obj.isBioLinkRequired = info.isBioLink

val protection = Protection(
kineticMax,
Expand All @@ -112,6 +113,7 @@ object StaticItemCreator {
applySkillMods(obj, info.skillMods)
obj.requiredCombatLevel = info.requiredLevel
obj.requiredFaction = ServerData.factions.getFaction(info.requiredFaction)
obj.isBioLinkRequired = info.isBioLink
applyColors(obj, info.color)
applyItemValue(info.value, obj)
}
Expand All @@ -120,6 +122,7 @@ object StaticItemCreator {
if (info == null)
return
obj.requiredCombatLevel = info.requiredLevel
obj.isBioLinkRequired = info.isBioLink

val weapon = obj as WeaponObject
weapon.type = info.weaponType
Expand Down Expand Up @@ -151,6 +154,7 @@ object StaticItemCreator {
obj.counter = info.charges
}

obj.isBioLinkRequired = info.bioLink
applyItemValue(info.value, obj)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public enum RadialHandler {
private final Map<String, RadialHandlerInterface> handlers = new HashMap<>();
private final Map<GameObjectType, RadialHandlerInterface> gotHandlers = new EnumMap<>(GameObjectType.class);
private final Map<Class<? extends SWGObject>, RadialHandlerInterface> classHandlers = new HashMap<>();

RadialHandler() {
initializeTerminalRadials();
initializeSurveyRadials();
Expand All @@ -66,7 +66,8 @@ public enum RadialHandler {
initializeSpecialEditionGoggleRadials();
initializeMeleeWeaponRadials();
initializeDeedRadials();

initializeBioLinkRadials();

RadialHandlerInterface aiHandler = new AIObjectRadial();

classHandlers.put(AIObject.class, aiHandler);
Expand Down Expand Up @@ -139,6 +140,13 @@ private void initializePetRadials() {
registerHandler(GameObjectType.GOT_VEHICLE_HOVER, new VehicleMountRadial());
}

private void initializeBioLinkRadials() {
RadialHandlerInterface bioLinkHandler = new BioLinkRadial();

registerHandler(GameObjectType.GOT_ARMOR, bioLinkHandler);
registerHandler(GameObjectType.GOT_WEAPON, bioLinkHandler);
}

private void initializeMiscRadials() {
registerHandler(GameObjectType.GOT_COMPONENT_SABER_CRYSTAL, new TuneCrystalRadial());
registerHandler("object/tangible/spawning/shared_spawn_egg.iff", new SpawnerRadial());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/***********************************************************************************
* 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.objects.radial.`object`

import com.projectswg.common.data.radial.RadialItem
import com.projectswg.common.data.radial.RadialOption
import com.projectswg.holocore.intents.gameplay.items.RequestBioLinkIntent
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.tangible.TangibleObject

class BioLinkRadial : SWGObjectRadial() {

override fun getOptions(options: MutableCollection<RadialOption>, player: Player, target: SWGObject) {
if (isLinkable(target)) {
options.add(RadialOption.create(RadialItem.BIO_LINK, "@ui_radial:bio_link"))
}
}

override fun handleSelection(player: Player, target: SWGObject, selection: RadialItem) {
if (selection != RadialItem.BIO_LINK) return
if (!isLinkable(target)) return

RequestBioLinkIntent(player.creatureObject, target as TangibleObject).broadcast() // Shows the confirmation window
}

private fun isLinkable(target: SWGObject): Boolean {
return target is TangibleObject && target.isBioLinkRequired && target.bioLinkedTo == null
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ public class TangibleObject extends SWGObject {
private String requiredSkill;
private DamageType lightsaberColorCrystalElementalType;
private int lightsaberColorCrystalDamagePercent;
private boolean bioLinkRequired;

private Map<String, Integer> skillMods = new LinkedHashMap<>();
private long lastCombat = 0;
Expand Down Expand Up @@ -376,6 +377,31 @@ public void setRequiredCombatLevel(int requiredCombatLevel) {
this.requiredCombatLevel = requiredCombatLevel;
}

/**
* @return {@code true} if this item must be bio-linked before it can be equipped or used
*/
public boolean isBioLinkRequired() {
return bioLinkRequired;
}

public void setBioLinkRequired(boolean bioLinkRequired) {
this.bioLinkRequired = bioLinkRequired;
}

/**
* @return object ID of the character this item is bio-linked to, or {@code null} if it isn't linked yet
*/
@Nullable
public Long getBioLinkedTo() {
if (!bioLinkRequired)
return null;
return (Long) getServerAttribute(ServerAttribute.LINK_OBJECT_ID);
}

public void setBioLinkedTo(long bioLinkedTo) {
setServerAttribute(ServerAttribute.LINK_OBJECT_ID, bioLinkedTo);
}

public Faction getRequiredFaction() {
return requiredFaction;
}
Expand Down Expand Up @@ -467,8 +493,10 @@ public AttributeList getAttributeList(CreatureObject viewer) {
attributeList.putNumber("cat_skill_mod_bonus.@stat_n:" + skillMod, value);
}

// TODO bio-link would go here, if this item is bio-link

if (bioLinkRequired) {
attributeList.putText("bio_link", getBioLinkAttributeValue());
}

if (getGameObjectType() == GameObjectType.GOT_COMPONENT_SABER_CRYSTAL) {
displayLightsaberCrystalAttributes(attributeList);
}
Expand Down Expand Up @@ -511,6 +539,22 @@ public AttributeList getAttributeList(CreatureObject viewer) {
return attributeList;
}

private String getBioLinkAttributeValue() {
Long bioLinkedTo = getBioLinkedTo();

if (bioLinkedTo == null) {
return "@obj_attr_n:bio_link_pending";
}

SWGObject owner = ObjectStorageService.ObjectLookup.getObjectById(bioLinkedTo);

if (owner == null) {
return "@obj_attr_n:bio_link_unknown";
}

return owner.getObjectName();
}

private boolean isOnlyWearableBySome(Set<Race> speciesRestrictions) {
return speciesRestrictions.size() != Race.values().length;
}
Expand Down Expand Up @@ -656,6 +700,7 @@ public void saveMongo(MongoData data) {
data.putString("lightsaberColorCrystalElementalType", lightsaberColorCrystalElementalType.name());
}
data.putInteger("lightsaberColorCrystalDamagePercent", lightsaberColorCrystalDamagePercent);
data.putBoolean("bioLinkRequired", bioLinkRequired);
}

@Override
Expand Down Expand Up @@ -694,6 +739,7 @@ public void readMongo(MongoData data) {
lightsaberColorCrystalElementalType = DamageType.valueOf(data.getString("lightsaberColorCrystalElementalType"));
}
lightsaberColorCrystalDamagePercent = data.getInteger("lightsaberColorCrystalDamagePercent", 0);
bioLinkRequired = data.getBoolean("bioLinkRequired", false);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import com.projectswg.holocore.services.gameplay.conversation.ConversationServic
import com.projectswg.holocore.services.gameplay.crafting.CraftingManager
import com.projectswg.holocore.services.gameplay.entertainment.EntertainmentManager
import com.projectswg.holocore.services.gameplay.faction.FactionManager
import com.projectswg.holocore.services.gameplay.items.BioLinkService
import com.projectswg.holocore.services.gameplay.jedi.JediManager
import com.projectswg.holocore.services.gameplay.junkdealer.JunkDealerService
import com.projectswg.holocore.services.gameplay.missions.DestroyMissionService
Expand All @@ -43,5 +44,5 @@ import com.projectswg.holocore.services.gameplay.world.WorldManager
import me.joshlarson.jlcommon.control.Manager
import me.joshlarson.jlcommon.control.ManagerStructure

@ManagerStructure(children = [BazaarService::class, CombatManager::class, ConversationService::class, CraftingManager::class, DeathCyberneticService::class, DestroyMissionService::class, EntertainmentManager::class, FactionManager::class, JediManager::class, JunkDealerService::class, PlayerManager::class, StructuresManager::class, TradeService::class, WorldManager::class])
@ManagerStructure(children = [BazaarService::class, BioLinkService::class, CombatManager::class, ConversationService::class, CraftingManager::class, DeathCyberneticService::class, DestroyMissionService::class, EntertainmentManager::class, FactionManager::class, JediManager::class, JunkDealerService::class, PlayerManager::class, StructuresManager::class, TradeService::class, WorldManager::class])
class GameplayManager : Manager()
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,16 @@ class BazaarService : Service() {
return
}

if (objectById is TangibleObject && objectById.bioLinkedTo != null) {
player.sendPacket(
CreateAuctionResponseMessage(
objectId = objectId, vendorId = packet.vendorId, status = 26
)
)

return
}

if (objectById.owner != player) {
player.sendPacket(
CreateAuctionResponseMessage(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/***********************************************************************************
* 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.services.gameplay.items

import com.projectswg.common.data.sui.SuiEvent
import com.projectswg.holocore.intents.gameplay.items.BioLinkNowIntent
import com.projectswg.holocore.intents.gameplay.items.RequestBioLinkIntent
import com.projectswg.holocore.intents.support.global.chat.SystemMessageIntent
import com.projectswg.holocore.resources.support.global.zone.sui.SuiButtons
import com.projectswg.holocore.resources.support.global.zone.sui.SuiMessageBox
import com.projectswg.holocore.resources.support.objects.swg.tangible.TangibleObject
import me.joshlarson.jlcommon.control.IntentHandler
import me.joshlarson.jlcommon.control.Service

class BioLinkService : Service() {

@IntentHandler
private fun handleRequestBioLinkIntent(intent: RequestBioLinkIntent) {
val linker = intent.linker
val item = intent.item
if (!isLinkable(item)) return
val owner = linker.owner ?: return

if (item.parent !== linker.inventory) {
SystemMessageIntent.broadcastPersonal(owner, "@base_player:must_bio_link_from_inventory")
return
}

SuiMessageBox().run {
title = "@sui:bio_link_item_title"
prompt = "@sui:bio_link_item_prompt"
buttons = SuiButtons.YES_NO
addOkButtonCallback("biolink") { _: SuiEvent, _: Map<String, String> -> BioLinkNowIntent(linker, item).broadcast() }
display(owner)
}
}

@IntentHandler
private fun handleBioLinkNowIntent(intent: BioLinkNowIntent) {
val linker = intent.linker
val item = intent.item
if (!isLinkable(item)) return

item.setBioLinkedTo(linker.objectId) // In case the name of the character ever changes
val owner = linker.owner ?: return
SystemMessageIntent.broadcastPersonal(owner, "@base_player:item_bio_linked")
}

private fun isLinkable(item: TangibleObject): Boolean {
return item.isBioLinkRequired && item.bioLinkedTo == null
}
}
Loading
Loading