diff --git a/src/services/combat/CorpseService.java b/src/services/combat/CorpseService.java index 31a3b8fdc..4b404c98f 100644 --- a/src/services/combat/CorpseService.java +++ b/src/services/combat/CorpseService.java @@ -27,16 +27,40 @@ ***********************************************************************************/ package services.combat; +import intents.chat.ChatBroadcastIntent; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import intents.combat.CreatureKilledIntent; import intents.object.DestroyObjectIntent; +import intents.object.ObjectCreatedIntent; +import intents.object.ObjectTeleportIntent; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; +import resources.Location; +import resources.Posture; import resources.control.Intent; import resources.control.Service; +import resources.encodables.ProsePackage; +import resources.encodables.StringId; +import resources.objects.SWGObject; +import resources.objects.building.BuildingObject; +import resources.objects.cell.CellObject; import resources.objects.creature.CreatureObject; +import resources.player.Player; import resources.server_info.Log; +import resources.server_info.RelationalDatabase; +import resources.server_info.RelationalServerFactory; +import resources.sui.SuiButtons; +import resources.sui.SuiEvent; +import resources.sui.SuiListBox; import utilities.ThreadUtilities; /** @@ -47,12 +71,23 @@ import utilities.ThreadUtilities; public final class CorpseService extends Service { private final ScheduledExecutorService executor; + private final Map facilityDataMap; + private final List cloningFacilities; + private final Random random; public CorpseService() { executor = Executors.newSingleThreadScheduledExecutor(ThreadUtilities.newThreadFactory("corpse-service")); + facilityDataMap = new HashMap<>(); + cloningFacilities = new ArrayList<>(); + random = new Random(); + registerForIntent(CreatureKilledIntent.TYPE); + registerForIntent(ObjectCreatedIntent.TYPE); + registerForIntent(DestroyObjectIntent.TYPE); + + loadFacilityData(); } - + @Override public boolean terminate() { executor.shutdown(); @@ -63,23 +98,148 @@ public final class CorpseService extends Service { public void onIntentReceived(Intent i) { switch(i.getType()) { case CreatureKilledIntent.TYPE: handleCreatureKilledIntent((CreatureKilledIntent) i); break; + case ObjectCreatedIntent.TYPE: handleObjectCreatedIntent((ObjectCreatedIntent) i); break; + case DestroyObjectIntent.TYPE: handleDestroyObjectIntent((DestroyObjectIntent) i); break; } } + private void loadFacilityData() { + long startTime = System.currentTimeMillis(); + Log.i(this, "Loading cloning facility data..."); + + try (RelationalDatabase spawnerDatabase = RelationalServerFactory.getServerData("player/cloning_respawn.db", "cloning_respawn")) { + try (ResultSet set = spawnerDatabase.executeQuery("SELECT * FROM cloning_respawn")) { + while (set.next()) { + int tubeCount = set.getInt("tubes"); + TubeData[] tubeData = new TubeData[tubeCount]; + + for(int i = 0; i < tubeCount; i++) { + String tubeName = String.format("tube%d", i + 1); + tubeData[i] = new TubeData(set.getFloat(tubeName + "_x"), set.getFloat(tubeName + "_z"), set.getFloat(tubeName + "_heading")); + } + + FacilityData facilityData = new FacilityData(set.getFloat("x"), set.getFloat("y"), set.getFloat("z"), set.getString("cell"), FacilityType.valueOf(set.getString("clone_type")), set.getString("stf_name"), tubeData); + String objectTemplate = set.getString("structure"); + + if(facilityDataMap.put(objectTemplate, facilityData) != null) { + // Duplicates are not allowed! + Log.e(this, "Duplicate entry for %s in row %d. Replacing previous entry with new", objectTemplate, set.getRow()); + } + } + } catch (SQLException e) { + Log.e(this, e); + } + } + + Log.i(this, "Finished loading cloning facility data for %d object templates. Time: %dms", facilityDataMap.size(), System.currentTimeMillis() - startTime); + } + private void handleCreatureKilledIntent(CreatureKilledIntent i) { CreatureObject killedCreature = i.getKilledCreature(); if(killedCreature.isPlayer()) { - // TODO show cloning system message - // TODO show cloning SUI window, with all possible facilities to clone at - // TODO after 30 minutes, close the SUI window and force them to clone at the nearest cloning facility - // TODO if the SUI window is closed by the player, make sure it reappears + Player killedCreatureOwner = killedCreature.getOwner(); + new ChatBroadcastIntent(killedCreatureOwner, new ProsePackage(new StringId("base_player", "prose_victim_dead"), "TT", i.getKillerCreature().getName())).broadcast(); + // TODO periodically remind the deathblown player that their timer is expiring. How often? + // TODO after 30 minutes, close the SUI window and force them to clone at the nearest cloning facility + SuiListBox window = new SuiListBox(SuiButtons.OK, "@base_player:revive_title", "@base_player:clone_prompt_header"); + + Collection facilitiesInTerrain = new ArrayList<>(); + + // Loop over clone facilities + for (BuildingObject cloningFacility : cloningFacilities) { + // A player can only clone into facilities in the same terrain as they're in themselves + if (cloningFacility.getLocation().getTerrain() == killedCreature.getLocation().getTerrain()) { + facilitiesInTerrain.add(cloningFacility); + } + } + + if (facilitiesInTerrain.isEmpty()) { + // Add options to SUI window + for (BuildingObject cloningFacility : facilitiesInTerrain) { + FacilityData facilityData = facilityDataMap.get(cloningFacility.getTemplate()); + + // TODO not all facilities use this + window.addListItem(facilityData.getStfName()); + } + + // TODO put below into a method pls + window.addCallback("handleFacilityChoice", (Player player, SWGObject actor, SuiEvent event, Map parameters) -> { + int selectionIndex = SuiListBox.getSelectedRow(parameters); + + if (event != SuiEvent.OK_PRESSED || selectionIndex + 1 > cloningFacilities.size() || selectionIndex < 0) { + window.display(player); + return; + } + + synchronized (cloningFacilities) { + BuildingObject selectedFacility = cloningFacilities.get(selectionIndex); + + FacilityData facilityData = facilityDataMap.get(selectedFacility.getTemplate()); + + if (facilityData != null) { + CreatureObject creature = player.getCreatureObject(); + String cellName = facilityData.getCell(); + CellObject cellObject = selectedFacility.getCellByName(cellName); + + if (cellObject != null) { + TubeData[] tubeData = facilityData.getTubeData(); + + TubeData randomData = tubeData[random.nextInt(tubeData.length)]; + Location tubeLocation = new Location(randomData.getTubeX(), 0, randomData.getTubeZ(), selectedFacility.getLocation().getTerrain()); + + new ObjectTeleportIntent(creature, cellObject, tubeLocation).broadcast(); + + creature.setPosture(Posture.UPRIGHT); + } else { + Log.e(this, "Cell %s was invalid for cloning facility %s", cellName, selectedFacility); + // TODO system message to player + } + // TODO NGE: cloning debuff + } else { + Log.e(this, "%s could not clone at facility %s because the object template is not in cloning_respawn.sdb", actor, selectedFacility); + // TODO system message to player + } + } + }); + + window.display(killedCreatureOwner); + } else { + + } } else { // This is a NPC - schedule corpse for deletion executor.schedule(() -> deleteCorpse(killedCreature), 120, TimeUnit.SECONDS); } } + private void handleObjectCreatedIntent(ObjectCreatedIntent i) { + SWGObject createdObject = i.getObject(); + if(createdObject instanceof BuildingObject) { + BuildingObject createdBuilding = (BuildingObject) createdObject; + String objectTemplate = createdBuilding.getTemplate(); + + FacilityData facilityData = facilityDataMap.get(objectTemplate); + + // If null, this object template isn't in cloning_respawn, which is fine + if(facilityData != null) { + synchronized(cloningFacilities) { + cloningFacilities.add(createdBuilding); + } + } + } + } + + private void handleDestroyObjectIntent(DestroyObjectIntent i) { + synchronized(cloningFacilities) { + SWGObject destroyedObject = i.getObject(); + + if(destroyedObject instanceof BuildingObject && cloningFacilities.remove((BuildingObject) destroyedObject)) { + Log.d(this, "Cloning facility %s was destroyed", destroyedObject); + } + } + } + /** * Only used for NPCs! * @param creatureCorpse non-player creature to delete from the world @@ -93,4 +253,83 @@ public final class CorpseService extends Service { } } + private static class FacilityData { + private final float x, y, z; + private final String cell; + private final FacilityType facilityType; + private final String stfName; + private final TubeData[] tubeData; + + public FacilityData(float x, float y, float z, String cell, FacilityType facilityType, String stfName, TubeData[] tubeData) { + this.x = x; + this.y = y; + this.z = z; + this.cell = cell; + this.facilityType = facilityType; + this.stfName = stfName; + this.tubeData = tubeData; + } + + public float getX() { + return x; + } + + public float getY() { + return y; + } + + public float getZ() { + return z; + } + + public String getCell() { + return cell; + } + + public FacilityType getFacilityType() { + return facilityType; + } + + public String getStfName() { + return stfName; + } + + public TubeData[] getTubeData() { + return tubeData; + } + } + + private static enum FacilityType { + STANDARD, + RESTRICTED, + PLAYER_CITY, + PRIVATE_INSTANCE, + FACTION_IMPERIAL, + FACTION_REBEL, + PVP_REGION_ADVANCED_IMPERIAL, + PVP_REGION_ADVANCED_REBEL + } + + private static class TubeData { + private final float tubeX, tubeZ, tubeHeading; + + public TubeData(float tubeX, float tubeZ, float tubeHeading) { + this.tubeX = tubeX; + this.tubeZ = tubeZ; + this.tubeHeading = tubeHeading; + } + + public float getTubeX() { + return tubeX; + } + + public float getTubeZ() { + return tubeZ; + } + + public float getTubeHeading() { + return tubeHeading; + } + } + } \ No newline at end of file