From f892f859d880e7417af7f7a367891474efbd3037 Mon Sep 17 00:00:00 2001 From: Mads Boddum Date: Sat, 12 Sep 2026 11:35:06 +0200 Subject: [PATCH 1/2] Added support for parking/vehicle garages in non-player cities --- .../intents/gameplay/world/PetIntents.kt | 1 + .../gameplay/structures/VehicleGarages.kt | 55 ++++++ .../radial/pet/VehicleMountRadial.java | 11 +- .../support/objects/swg/SWGObject.java | 9 +- .../gameplay/structures/StructuresManager.kt | 3 +- .../structures/VehicleGarageService.kt | 162 ++++++++++++++++++ 6 files changed, 238 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/projectswg/holocore/resources/gameplay/structures/VehicleGarages.kt create mode 100644 src/main/java/com/projectswg/holocore/services/gameplay/structures/VehicleGarageService.kt diff --git a/src/main/java/com/projectswg/holocore/intents/gameplay/world/PetIntents.kt b/src/main/java/com/projectswg/holocore/intents/gameplay/world/PetIntents.kt index 75887c7f2..caf286b17 100644 --- a/src/main/java/com/projectswg/holocore/intents/gameplay/world/PetIntents.kt +++ b/src/main/java/com/projectswg/holocore/intents/gameplay/world/PetIntents.kt @@ -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() diff --git a/src/main/java/com/projectswg/holocore/resources/gameplay/structures/VehicleGarages.kt b/src/main/java/com/projectswg/holocore/resources/gameplay/structures/VehicleGarages.kt new file mode 100644 index 000000000..b1d09913a --- /dev/null +++ b/src/main/java/com/projectswg/holocore/resources/gameplay/structures/VehicleGarages.kt @@ -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 . * + ***********************************************************************************/ +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() + .filter { it.template in TEMPLATES } + .minByOrNull { it.worldLocation.flatDistanceTo(playerLocation) } + ?.takeIf { it.worldLocation.flatDistanceTo(playerLocation) <= RANGE } + } +} diff --git a/src/main/java/com/projectswg/holocore/resources/support/objects/radial/pet/VehicleMountRadial.java b/src/main/java/com/projectswg/holocore/resources/support/objects/radial/pet/VehicleMountRadial.java index c20e17336..d8af95b2c 100644 --- a/src/main/java/com/projectswg/holocore/resources/support/objects/radial/pet/VehicleMountRadial.java +++ b/src/main/java/com/projectswg/holocore/resources/support/objects/radial/pet/VehicleMountRadial.java @@ -31,6 +31,7 @@ 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.support.global.player.Player; import com.projectswg.holocore.resources.support.objects.radial.RadialHandlerInterface; @@ -38,6 +39,7 @@ 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.resources.gameplay.structures.VehicleGarages; import com.projectswg.holocore.services.support.objects.ObjectStorageService.ObjectLookup; import org.jetbrains.annotations.NotNull; @@ -61,8 +63,12 @@ public void getOptions(@NotNull Collection options, @NotNull Playe else options.add(RadialOption.create(RadialItem.ITEM_USE, "@cmd_n:mount")); - if (creature.getObjectId() == mount.getOwnerId()) + 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 @@ -81,6 +87,9 @@ public void handleSelection(@NotNull Player player, @NotNull SWGObject target, @ case PET_STORE: new StoreMountIntent(creature, (CreatureObject) target).broadcast(); break; + case SERVER_MENU1: + new RepairVehicleIntent(creature, (CreatureObject) target).broadcast(); + break; default: break; } diff --git a/src/main/java/com/projectswg/holocore/resources/support/objects/swg/SWGObject.java b/src/main/java/com/projectswg/holocore/resources/support/objects/swg/SWGObject.java index 61c81352a..bfa305395 100644 --- a/src/main/java/com/projectswg/holocore/resources/support/objects/swg/SWGObject.java +++ b/src/main/java/com/projectswg/holocore/resources/support/objects/swg/SWGObject.java @@ -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 diff --git a/src/main/java/com/projectswg/holocore/services/gameplay/structures/StructuresManager.kt b/src/main/java/com/projectswg/holocore/services/gameplay/structures/StructuresManager.kt index 28ba6be8a..706bbd5a9 100644 --- a/src/main/java/com/projectswg/holocore/services/gameplay/structures/StructuresManager.kt +++ b/src/main/java/com/projectswg/holocore/services/gameplay/structures/StructuresManager.kt @@ -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() diff --git a/src/main/java/com/projectswg/holocore/services/gameplay/structures/VehicleGarageService.kt b/src/main/java/com/projectswg/holocore/services/gameplay/structures/VehicleGarageService.kt new file mode 100644 index 000000000..1962d89e5 --- /dev/null +++ b/src/main/java/com/projectswg/holocore/services/gameplay/structures/VehicleGarageService.kt @@ -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 . * + ***********************************************************************************/ +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 = 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) { + 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" +} From 0bf1edddea2ba79a95efbd85a2462f5ee9420547 Mon Sep 17 00:00:00 2001 From: Mads Boddum Date: Sat, 12 Sep 2026 12:05:27 +0200 Subject: [PATCH 2/2] Convert VehicleMountRadial to Kotlin --- ...MountRadial.java => VehicleMountRadial.kt} | 121 +++++++----------- 1 file changed, 48 insertions(+), 73 deletions(-) rename src/main/java/com/projectswg/holocore/resources/support/objects/radial/pet/{VehicleMountRadial.java => VehicleMountRadial.kt} (58%) diff --git a/src/main/java/com/projectswg/holocore/resources/support/objects/radial/pet/VehicleMountRadial.java b/src/main/java/com/projectswg/holocore/resources/support/objects/radial/pet/VehicleMountRadial.kt similarity index 58% rename from src/main/java/com/projectswg/holocore/resources/support/objects/radial/pet/VehicleMountRadial.java rename to src/main/java/com/projectswg/holocore/resources/support/objects/radial/pet/VehicleMountRadial.kt index d8af95b2c..befb45c09 100644 --- a/src/main/java/com/projectswg/holocore/resources/support/objects/radial/pet/VehicleMountRadial.java +++ b/src/main/java/com/projectswg/holocore/resources/support/objects/radial/pet/VehicleMountRadial.kt @@ -24,94 +24,69 @@ * 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.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, 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.RepairVehicleIntent; -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.resources.gameplay.structures.VehicleGarages; -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 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.getObjectId() == mount.getOwnerId()) { - options.add(RadialOption.create(RadialItem.PET_STORE)); + if (creature.objectId == target.ownerId) { + 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")); + 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; - case SERVER_MENU1: - new RepairVehicleIntent(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 } - }