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
@@ -1,5 +1,5 @@
/***********************************************************************************
* Copyright (c) 2024 /// Project SWG /// www.projectswg.com *
* 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. *
Expand Down Expand Up @@ -42,24 +42,41 @@ import com.projectswg.holocore.resources.support.objects.swg.custom.AIObject
import com.projectswg.holocore.services.gameplay.crafting.resource.HarvestBoneIntent
import com.projectswg.holocore.services.gameplay.crafting.resource.HarvestHideIntent
import com.projectswg.holocore.services.gameplay.crafting.resource.HarvestMeatIntent
import com.projectswg.holocore.services.gameplay.crafting.resource.MilkCreatureIntent

class AIObjectRadial : RadialHandlerInterface {
override fun getOptions(options: MutableCollection<RadialOption>, player: Player, target: SWGObject) {
val ai = target as AIObject
if (ai.posture != Posture.DEAD) {
return
}
val creatureObject = player.creatureObject
if (Locomotion.DEAD.isActive(creatureObject) || Locomotion.INCAPACITATED.isActive(creatureObject)) {
return
}
if (ai.posture == Posture.DEAD) {
appendDeadCreatureOptions(ai, creatureObject, options)
} else {
appendLivingCreatureOptions(ai, options)
}
}

private fun appendDeadCreatureOptions(ai: AIObject, creatureObject: CreatureObject, options: MutableCollection<RadialOption>) {
options.add(RadialOption.create(RadialItem.LOOT_ALL, RadialOption.create(RadialItem.LOOT)))

if (isScout(creatureObject)) {
appendScoutOptions(ai, options)
}
}

private fun appendLivingCreatureOptions(ai: AIObject, options: MutableCollection<RadialOption>) {
if (ai.isMilked || ai.ownerId > 0) {
return
}
val npcInfo = ServerData.npcs[ai.creatureId ?: return] ?: return

if (npcInfo.milkResourceInfo.amount > 0) {
options.add(RadialOption.create(RadialItem.SERVER_MENU4, "@pet/pet_menu:milk_me"))
}
}

private fun appendScoutOptions(ai: AIObject, options: MutableCollection<RadialOption>) {
val npcInfo = ServerData.npcs[ai.creatureId ?: return] ?: return
val harvestCreatureOptions = harvestCreatureOptions(npcInfo)
Expand Down Expand Up @@ -94,13 +111,18 @@ class AIObjectRadial : RadialHandlerInterface {

override fun handleSelection(player: Player, target: SWGObject, selection: RadialItem) {
val ai = target as AIObject
if (ai.posture != Posture.DEAD) {
return
}
val creatureObject = player.creatureObject
if (Locomotion.DEAD.isActive(creatureObject) || Locomotion.INCAPACITATED.isActive(creatureObject)) {
return
}
if (ai.posture == Posture.DEAD) {
handleDeadCreatureSelection(player, ai, selection)
} else {
handleLivingCreatureSelection(player, ai, selection)
}
}

private fun handleDeadCreatureSelection(player: Player, ai: AIObject, selection: RadialItem) {
when (selection) {
RadialItem.LOOT -> LootRequestIntent(player, ai, LootType.LOOT).broadcast()
RadialItem.LOOT_ALL -> LootRequestIntent(player, ai, LootType.LOOT_ALL).broadcast()
Expand All @@ -110,4 +132,11 @@ class AIObjectRadial : RadialHandlerInterface {
else -> StandardLog.onPlayerError(this, player, "radial option without handling selected: %s", selection)
}
}

private fun handleLivingCreatureSelection(player: Player, ai: AIObject, selection: RadialItem) {
when (selection) {
RadialItem.SERVER_MENU4 -> MilkCreatureIntent(player, ai).broadcast()
else -> StandardLog.onPlayerError(this, player, "radial option without handling selected: %s", selection)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/***********************************************************************************
* Copyright (c) 2025 /// Project SWG /// www.projectswg.com *
* 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. *
Expand Down Expand Up @@ -77,6 +77,7 @@ class AIObject(objectId: Long) : CreatureObject(objectId) {
var spawner: Spawner? = null
var creatureId: String? = null
var isHarvested: Boolean = false
var isMilked: Boolean = false

override fun onObjectEnteredAware(aware: SWGObject) {
val player = getCreatureOrRider(aware) ?: return
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/***********************************************************************************
* Copyright (c) 2023 /// Project SWG /// www.projectswg.com *
* Copyright (c) 2026 /// 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. *
Expand Down Expand Up @@ -27,10 +27,11 @@
package com.projectswg.holocore.services.gameplay.crafting

import com.projectswg.holocore.services.gameplay.crafting.resource.CreatureHarvestingService
import com.projectswg.holocore.services.gameplay.crafting.resource.CreatureMilkingService
import com.projectswg.holocore.services.gameplay.crafting.resource.ResourceService
import com.projectswg.holocore.services.gameplay.crafting.survey.SurveyToolService
import me.joshlarson.jlcommon.control.Manager
import me.joshlarson.jlcommon.control.ManagerStructure

@ManagerStructure(children = [CreatureHarvestingService::class, ResourceService::class, SurveyToolService::class])
@ManagerStructure(children = [CreatureHarvestingService::class, CreatureMilkingService::class, ResourceService::class, SurveyToolService::class])
class CraftingManager : Manager()
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/***********************************************************************************
* Copyright (c) 2023 /// Project SWG /// www.projectswg.com *
* Copyright (c) 2026 /// 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. *
Expand Down Expand Up @@ -33,3 +33,4 @@ import me.joshlarson.jlcommon.control.Intent
data class HarvestBoneIntent(val player: Player, val target: AIObject): Intent()
data class HarvestHideIntent(val player: Player, val target: AIObject): Intent()
data class HarvestMeatIntent(val player: Player, val target: AIObject): Intent()
data class MilkCreatureIntent(val player: Player, val target: AIObject): Intent()
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
/***********************************************************************************
* 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.crafting.resource

import com.projectswg.common.data.encodables.tangible.Posture
import com.projectswg.holocore.intents.support.global.chat.SystemMessageIntent
import com.projectswg.holocore.resources.gameplay.crafting.resource.galactic.GalacticResource
import com.projectswg.holocore.resources.gameplay.crafting.resource.galactic.storage.GalacticResourceContainer.getRawResource
import com.projectswg.holocore.resources.gameplay.crafting.resource.galactic.storage.GalacticResourceContainer.getSpawnedResources
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.global.player.Player
import com.projectswg.holocore.resources.support.objects.swg.creature.CreatureState
import com.projectswg.holocore.resources.support.objects.swg.custom.AIObject
import com.projectswg.holocore.utilities.HolocoreCoroutine
import com.projectswg.holocore.utilities.cancelAndWait
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import me.joshlarson.jlcommon.control.IntentHandler
import me.joshlarson.jlcommon.control.Service
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ThreadLocalRandom

class CreatureMilkingService(private val milkingDurationMs: LongRange) : Service() {

constructor() : this(MILKING_DURATION_MS)

private val milkingScope = HolocoreCoroutine.childScope()
private val creaturesBeingMilked: MutableSet<AIObject> = ConcurrentHashMap.newKeySet()

override fun stop(): Boolean {
milkingScope.cancelAndWait()
return super.stop()
}

@IntentHandler
private fun handleMilkCreatureIntent(intent: MilkCreatureIntent) {
val ai = intent.target
val player = intent.player
val creature = player.creatureObject

val npcInfo = ServerData.npcs[ai.creatureId ?: return] ?: return
val milkResourceInfo = npcInfo.milkResourceInfo
if (milkResourceInfo.amount <= 0) return
if (ai.ownerId > 0) return // Tamed creatures can not be milked

if (creature.isStatesBitmask(CreatureState.RIDING_MOUNT)) {
SystemMessageIntent.broadcastPersonal(player, "@skl_use:skl_use")
return
}
if (ai.isMilked) {
SystemMessageIntent.broadcastPersonal(player, "@skl_use:milk_cant")
return
}
if (!isAbleToContinueMilking(player, ai)) {
SystemMessageIntent.broadcastPersonal(player, "@skl_use:milk_too_far")
return
}
if (!creaturesBeingMilked.add(ai)) {
SystemMessageIntent.broadcastPersonal(player, "@skl_use:being_milked")
return
}

val spawnedResource = getSpawnedResource(player, milkResourceInfo.type)
if (spawnedResource == null) {
creaturesBeingMilked.remove(ai)
StandardLog.onPlayerError(this, player, "unable to find a creature resource of type %s for %s", milkResourceInfo.type, ai)
return
}

val amount = milkResourceInfo.amount
milkingScope.launch {
try {
milk(player, ai, spawnedResource, amount)
} finally {
creaturesBeingMilked.remove(ai)
}
}
}

private suspend fun milk(player: Player, ai: AIObject, resource: GalacticResource, amount: Int) {
val tickDelay = ThreadLocalRandom.current().nextLong(milkingDurationMs.first, milkingDurationMs.last + 1) / TICKS
SystemMessageIntent.broadcastPersonal(player, "@skl_use:milk_begin")
StandardLog.onPlayerTrace(this, player, "started milking %d %s from %s with a tick delay of %d ms", amount, resource.name, ai, tickDelay)

for (tick in 1..TICKS) {
delay(tickDelay)

if (!isAbleToContinueMilking(player, ai)) {
SystemMessageIntent.broadcastPersonal(player, "@skl_use:milk_too_far")
StandardLog.onPlayerTrace(this, player, "aborted milking %s on tick %d of %d", ai, tick, TICKS)
return
}

if (tick < TICKS) {
SystemMessageIntent.broadcastPersonal(player, "@skl_use:milk_continue")
}
}

val service = this
val eventHandler = object : ResourceContainerEventHandler {
override fun onUnknownError() {

}

override fun onInventoryFull() {
SystemMessageIntent.broadcastPersonal(player, "@container_error_message:container03")
}

override fun onSuccess() {
ai.isMilked = true
StandardLog.onPlayerEvent(service, player, "milked %d %s from %s", amount, resource.name, ai)
SystemMessageIntent.broadcastPersonal(player, "@skl_use:milk_success")
}
}
ResourceContainerHelper.giveResourcesToPlayer(amount, resource, player, eventHandler)
}

private fun isAbleToContinueMilking(player: Player, ai: AIObject): Boolean {
val creature = player.creatureObject

if (ai.posture == Posture.DEAD || creature.posture == Posture.DEAD || creature.posture == Posture.INCAPACITATED) return false
if (creature.isInCombat || ai.isInCombat) return false
if (creature.terrain != ai.terrain) return false

return creature.worldLocation.distanceTo(ai.worldLocation) <= MILKING_RANGE
}

private fun getSpawnedResource(player: Player, requestedCreatureResourceType: String): GalacticResource? {
val spawnedResources = getSpawnedResources(player.creatureObject.terrain)
for (spawnedResource in spawnedResources) {
val rawResource = getRawResource(spawnedResource.rawResourceId) ?: continue

if (rawResource.name.startsWith("${requestedCreatureResourceType}_")) {
return spawnedResource
}
}

return null
}

companion object {
private const val MILKING_RANGE = 5.0
private const val TICKS = 3
private val MILKING_DURATION_MS = 20_000L..30_000L
}
}
Loading
Loading