Skip to content
Open
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
Expand Up @@ -35,5 +35,6 @@ data class DismountIntent(val creature: CreatureObject, val pet: CreatureObject)
data class MountIntent(val creature: CreatureObject, val pet: CreatureObject) : Intent()
data class PetDeviceCallIntent(val creature: CreatureObject, val controlDevice: IntangibleObject) : Intent()
data class PetDeviceStoreIntent(val creature: CreatureObject, val controlDevice: IntangibleObject) : Intent()
data class RepairVehicleIntent(val creature: CreatureObject, val pet: CreatureObject) : Intent()
data class StoreMountIntent(val creature: CreatureObject, val pet: CreatureObject) : Intent()
data class VehicleDeedGenerateIntent(val creature: CreatureObject, val deed: SWGObject) : Intent()
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/***********************************************************************************
* 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.gameplay.structures

import com.projectswg.holocore.resources.support.objects.swg.building.BuildingObject
import com.projectswg.holocore.resources.support.objects.swg.creature.CreatureObject

object VehicleGarages {

private const val RANGE = 64.0

private val TEMPLATES = setOf(
"object/building/base/shared_base_parking_garage.iff",
"object/building/tatooine/shared_parking_garage_tatooine_style_1.iff",
"object/building/naboo/shared_parking_garage_naboo_style_1.iff",
"object/building/general/shared_parking_garage_general.iff",
"object/building/corellia/shared_garage_corellia.iff",
"object/building/player/shared_player_garage_tatooine_style_01.iff",
"object/building/player/shared_player_garage_naboo_style_01.iff",
"object/building/player/shared_player_garage_corellia_style_01.iff",
)

@JvmStatic
fun nearestGarage(player: CreatureObject): BuildingObject? {
val playerLocation = player.worldLocation
return player.aware.asSequence()
.filterIsInstance<BuildingObject>()
.filter { it.template in TEMPLATES }
.minByOrNull { it.worldLocation.flatDistanceTo(playerLocation) }
?.takeIf { it.worldLocation.flatDistanceTo(playerLocation) <= RANGE }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,85 +24,69 @@
* 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.pet

import com.projectswg.common.data.radial.RadialItem
import com.projectswg.common.data.radial.RadialOption
import com.projectswg.holocore.intents.gameplay.world.DismountIntent
import com.projectswg.holocore.intents.gameplay.world.MountIntent
import com.projectswg.holocore.intents.gameplay.world.RepairVehicleIntent
import com.projectswg.holocore.intents.gameplay.world.StoreMountIntent
import com.projectswg.holocore.resources.gameplay.structures.VehicleGarages
import com.projectswg.holocore.resources.support.global.player.Player
import com.projectswg.holocore.resources.support.objects.radial.RadialHandlerInterface
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.creature.CreatureState
import com.projectswg.holocore.resources.support.objects.swg.group.GroupObject
import com.projectswg.holocore.services.support.objects.ObjectStorageService.ObjectLookup

class VehicleMountRadial : RadialHandlerInterface {

override fun getOptions(options: MutableCollection<RadialOption>, player: Player, target: SWGObject) {
val creature = player.creatureObject
if (target !is CreatureObject || !isValidTarget(creature, target)) return

options.add(RadialOption.create(RadialItem.EXAMINE))
if (creature.isStatesBitmask(CreatureState.RIDING_MOUNT) && creature.parent === target) {
options.add(RadialOption.create(RadialItem.ITEM_USE, "@cmd_n:dismount"))
} else {
options.add(RadialOption.create(RadialItem.ITEM_USE, "@cmd_n:mount"))
}

package com.projectswg.holocore.resources.support.objects.radial.pet;

import com.projectswg.common.data.radial.RadialItem;
import com.projectswg.common.data.radial.RadialOption;
import com.projectswg.holocore.intents.gameplay.world.DismountIntent;
import com.projectswg.holocore.intents.gameplay.world.MountIntent;
import com.projectswg.holocore.intents.gameplay.world.StoreMountIntent;
import com.projectswg.holocore.resources.support.global.player.Player;
import com.projectswg.holocore.resources.support.objects.radial.RadialHandlerInterface;
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.creature.CreatureState;
import com.projectswg.holocore.resources.support.objects.swg.group.GroupObject;
import com.projectswg.holocore.services.support.objects.ObjectStorageService.ObjectLookup;
import org.jetbrains.annotations.NotNull;

import java.util.Collection;

public class VehicleMountRadial implements RadialHandlerInterface {

public VehicleMountRadial() {

}

@Override
public void getOptions(@NotNull Collection<RadialOption> options, @NotNull Player player, @NotNull SWGObject target) {
CreatureObject creature = player.getCreatureObject();
if (!(target instanceof CreatureObject mount) || !isValidTarget(creature, (CreatureObject) target))
return;

options.add(RadialOption.create(RadialItem.EXAMINE));
if (creature.isStatesBitmask(CreatureState.RIDING_MOUNT) && creature.getParent() == target)
options.add(RadialOption.create(RadialItem.ITEM_USE, "@cmd_n:dismount"));
else
options.add(RadialOption.create(RadialItem.ITEM_USE, "@cmd_n:mount"));
if (creature.objectId == target.ownerId) {
options.add(RadialOption.create(RadialItem.PET_STORE))

if (creature.getObjectId() == mount.getOwnerId())
options.add(RadialOption.create(RadialItem.PET_STORE));
if (VehicleGarages.nearestGarage(creature) != null) {
options.add(RadialOption.create(RadialItem.SERVER_MENU1, "@pet/pet_menu:menu_repair_vehicle"))
}
}
}

@Override
public void handleSelection(@NotNull Player player, @NotNull SWGObject target, @NotNull RadialItem selection) {
CreatureObject creature = player.getCreatureObject();
if (!(target instanceof CreatureObject) || !isValidTarget(creature, (CreatureObject) target))
return;
override fun handleSelection(player: Player, target: SWGObject, selection: RadialItem) {
val creature = player.creatureObject
if (target !is CreatureObject || !isValidTarget(creature, target)) return

switch (selection) {
case ITEM_USE:
if (player.getCreatureObject().getParent() == target)
new DismountIntent(creature, (CreatureObject) target).broadcast();
else
new MountIntent(creature, (CreatureObject) target).broadcast();
break;
case PET_STORE:
new StoreMountIntent(creature, (CreatureObject) target).broadcast();
break;
default:
break;
when (selection) {
RadialItem.ITEM_USE -> if (creature.parent === target) DismountIntent(creature, target).broadcast() else MountIntent(creature, target).broadcast()
RadialItem.PET_STORE -> StoreMountIntent(creature, target).broadcast()
RadialItem.SERVER_MENU1 -> RepairVehicleIntent(creature, target).broadcast()
else -> {}
}
}

private static boolean isValidTarget(CreatureObject creature, CreatureObject mount) {
private fun isValidTarget(creature: CreatureObject, mount: CreatureObject): Boolean {
// Owner of the vehicle
if (creature.getObjectId() == mount.getOwnerId())
return true;
if (creature.objectId == mount.ownerId) return true

// Already mounted
if (creature.getParent() == mount && mount.isStatesBitmask(CreatureState.MOUNTED_CREATURE) && creature.isStatesBitmask(CreatureState.RIDING_MOUNT))
return true;
if (creature.parent === mount && mount.isStatesBitmask(CreatureState.MOUNTED_CREATURE) && creature.isStatesBitmask(CreatureState.RIDING_MOUNT)) return true

// Within the same group
GroupObject group = (GroupObject) ObjectLookup.getObjectById(creature.getGroupId());
if (group == null || !group.isInGroup(mount.getOwnerId())) {
return false;
}
val group = ObjectLookup.getObjectById(creature.groupId) as GroupObject?
if (group == null || !group.isInGroup(mount.ownerId)) return false

// Owner is already mounted
return mount.getSlottedObject("rider") != null;
return mount.getSlottedObject("rider") != null
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -664,7 +664,14 @@ public boolean removeFromBankAndCash(long amount) {
}
return true;
}


public boolean removeFromBank(long amount) {
if (bankBalance < amount)
return false;
setBankBalance(bankBalance - amount);
return true;
}

/**
* Adds amount to cash balance.
* @param amount the amount to add
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import me.joshlarson.jlcommon.control.ManagerStructure
HousingManager::class,
StructureService::class,
ElevatorService::class,
MedicalCenterService::class
MedicalCenterService::class,
VehicleGarageService::class
])
class StructuresManager : Manager()
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
/***********************************************************************************
* 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.structures

import com.projectswg.common.data.encodables.oob.ProsePackage
import com.projectswg.common.data.encodables.oob.StringId
import com.projectswg.holocore.intents.gameplay.world.RepairVehicleIntent
import com.projectswg.holocore.intents.gameplay.world.StoreMountIntent
import com.projectswg.holocore.intents.support.global.chat.SystemMessageIntent
import com.projectswg.holocore.intents.support.global.chat.SystemMessageIntent.Companion.broadcastPersonal
import com.projectswg.holocore.intents.support.global.zone.PlayerEventIntent
import com.projectswg.holocore.intents.support.global.zone.PlayerTransformedIntent
import com.projectswg.holocore.resources.gameplay.structures.VehicleGarages.nearestGarage
import com.projectswg.holocore.resources.support.data.server_info.StandardLog
import com.projectswg.holocore.resources.support.data.server_info.loader.DataLoader.Companion.vehicles
import com.projectswg.holocore.resources.support.data.server_info.loader.VehicleLoader.VehicleInfo
import com.projectswg.holocore.resources.support.global.player.PlayerEvent
import com.projectswg.holocore.resources.support.global.zone.sui.SuiButtons
import com.projectswg.holocore.resources.support.global.zone.sui.SuiListBox
import com.projectswg.holocore.resources.support.objects.swg.creature.CreatureObject
import com.projectswg.holocore.resources.support.objects.swg.creature.CreatureState
import me.joshlarson.jlcommon.control.IntentHandler
import me.joshlarson.jlcommon.control.Service
import java.util.concurrent.ConcurrentHashMap
import kotlin.math.roundToInt
import kotlin.math.roundToLong

class VehicleGarageService : Service() {

private val playersNotifiedOfGarage: MutableSet<CreatureObject> = ConcurrentHashMap.newKeySet()

@IntentHandler
private fun handlePlayerTransformedIntent(pti: PlayerTransformedIntent) {
val player = pti.player
if (!player.isStatesBitmask(CreatureState.RIDING_MOUNT)) return
val owner = player.owner ?: return

if (nearestGarage(player) != null) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't really be scanning the entire awareness of a player every time they move/rotate. Need to hook into the on awareness changed functionality in each CreatureObject somehow. I'm leaning towards an intent notifying on player awareness changed

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, this would be much more efficient.

if (playersNotifiedOfGarage.add(player)) broadcastPersonal(owner, "@pet/pet_menu:garage_proximity")
} else {
playersNotifiedOfGarage.remove(player)
}
}

@IntentHandler
private fun handleStoreMountIntent(smi: StoreMountIntent) {
playersNotifiedOfGarage.remove(smi.creature)
}

@IntentHandler
private fun handlePlayerEventIntent(pei: PlayerEventIntent) {
if (pei.event == PlayerEvent.PE_DISAPPEAR) playersNotifiedOfGarage.remove(pei.player.creatureObject)
}

@IntentHandler
private fun handleRepairVehicleIntent(rvi: RepairVehicleIntent) {
val creature = rvi.creature
val mount = rvi.pet
val owner = creature.owner ?: return

if (creature.objectId != mount.ownerId) return
if (nearestGarage(creature) == null) {
broadcastPersonal(owner, "@pet/pet_menu:repair_unrecognized_garages")
return
}

val damage = mount.conditionDamage
if (damage <= 0) {
broadcastPersonal(owner, "@pet/pet_menu:undamaged_vehicle")
return
}

val vehicleInfo = vehicles().getVehicleFromIff(mount.template)
if (vehicleInfo == null || vehicleInfo.repairRate <= 0) {
StandardLog.onPlayerError(this, creature, "unable to determine repair rate for vehicle %s", mount)
broadcastPersonal(owner, "@pet/pet_menu:repair_error")
return
}

if (damage >= mount.maxHitPoints && !vehicleInfo.isCanRepairDisabled) {
broadcastPersonal(owner, "@pet/pet_menu:cannot_repair_disabled")
return
}

val cost = repairCost(damage, vehicleInfo.repairRate)
val funds = creature.bankBalance
if (funds < cost) {
broadcastPersonal(owner, lackingFunds(cost - funds))
return
}

SuiListBox().run {
title = "@pet/pet_menu:confirm_repairs_t"
prompt = "@pet/pet_menu:vehicle_repair_d"
buttons = SuiButtons.OK_CANCEL
addListItem("@pet/pet_menu:vehicle_prompt ${mount.stringId}")
addListItem("@pet/pet_menu:repair_cost_prompt $cost")
addListItem("@pet/pet_menu:total_funds_prompt $funds")
addOkButtonCallback("repair") { _, _ -> repair(creature, mount, cost, vehicleInfo) }
display(owner)
}
}

private fun repair(creature: CreatureObject, mount: CreatureObject, cost: Long, vehicleInfo: VehicleInfo) {
val owner = creature.owner ?: return

if (nearestGarage(creature) == null) {
broadcastPersonal(owner, "@pet/pet_menu:garage_out_of_range")
return
}

val funds = creature.bankBalance
if (!creature.removeFromBank(cost)) {
broadcastPersonal(owner, lackingFunds(cost - funds))
return
}

val damage = mount.conditionDamage
val paidForPoints = (cost / vehicleInfo.repairRate).roundToInt()
val repairedPoints = paidForPoints.coerceAtMost(damage)
var paid = cost

// Condition may have improved between opening the window and confirming - only charge for what's actually repaired
if (repairedPoints < paidForPoints) {
paid = repairCost(repairedPoints, vehicleInfo.repairRate)
creature.addToBank(cost - paid)
SystemMessageIntent.broadcastPersonal(owner, ProsePackage(StringId("pet/pet_menu", "repair_condition_changed"), "DI", paid.toInt()))
}

SystemMessageIntent.broadcastPersonal(owner, ProsePackage(StringId("base_player", "prose_pay_success_no_target"), "DI", paid.toInt()))

mount.conditionDamage = damage - repairedPoints
StandardLog.onPlayerEvent(this, creature, "repaired mount %s for %d credits, condition is now %d/%d", mount, paid, mount.maxHitPoints - mount.conditionDamage, mount.maxHitPoints)
}

private fun repairCost(points: Int, repairRate: Double): Long = (points * repairRate).roundToLong()

private fun lackingFunds(missing: Long): String = "@pet/pet_menu:lacking_funds_prefix $missing @pet/pet_menu:lacking_funds_suffix"
}