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
4 changes: 4 additions & 0 deletions src/main/java/net/tfminecraft/geminfusion/InfusionBlock.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,17 @@
import java.util.List;

import org.bukkit.Location;
import org.bukkit.inventory.ItemStack;

public class InfusionBlock {
public Location loc;
public boolean token;
public Location particleLoc;
public Integer infusionHits;
public List<Gemstone> currentGems = new ArrayList<Gemstone>();
public List<Drop> pendingDrops = new ArrayList<>();

public record Drop(ItemStack item, String announcement) {}

//Setters
public void setToken(boolean b) {
Expand Down
142 changes: 97 additions & 45 deletions src/main/java/net/tfminecraft/geminfusion/InfusionEvents.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package net.tfminecraft.geminfusion;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;

import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
Expand Down Expand Up @@ -32,6 +32,49 @@

public class InfusionEvents implements Listener{
public List<InfusionBlock> currentStations = new ArrayList<InfusionBlock>();
private final InfusionStationStore store;
private final List<BukkitRunnable> deliveries = new ArrayList<>();

public InfusionEvents() {
this(new InfusionStationStore());
}

InfusionEvents(InfusionStationStore store) {
this.store = store;
}

public void loadPersisted() {
for (BukkitRunnable delivery : deliveries) delivery.cancel();
deliveries.clear();
currentStations.clear();
currentStations.addAll(store.loadAll());
for (InfusionBlock station : currentStations) {
if (station.getInfusionHits() == 5) scheduleDrops(station);
}
}

public void flush() {
for (InfusionBlock station : currentStations) store.save(station);
}

public void shutdown() {
for (BukkitRunnable delivery : deliveries) delivery.cancel();
deliveries.clear();
flush();
}

private boolean deposit(Player player, InfusionBlock station, Gemstone gem) {
station.addGem(gem);
if (!store.save(station)) {
station.getCurrentItems().removeLast();
player.sendMessage(ChatColor.RED + "This infusion table could not be saved. Contact staff; your gem remains in your hand.");
return false;
}
ItemStack hand = player.getInventory().getItemInMainHand();
hand.setAmount(hand.getAmount() - 1);
return true;
}

// Keep the existing legacy text representation, formatting, and exact-string comparisons.
@SuppressWarnings("deprecation")
@EventHandler
Expand Down Expand Up @@ -94,8 +137,7 @@ public void addGemEvent(PlayerInteractEvent e) {
}
if(loc.equals(b.getLocation())) {
exists = true;
p.getInventory().getItemInMainHand().setAmount(p.getInventory().getItemInMainHand().getAmount()-1);
b.addGem(gem);
if (!deposit(p, b, gem)) return;
p.sendTitle(ChatColor.LIGHT_PURPLE + "Added " + gem.getColour() + gem.getName(), ChatColor.GRAY + "Current Gem Amount: " + ChatColor.YELLOW + b.getCurrentItems().size()+"/10", 1, 40, 20);
b.getLocation().getWorld().spawnParticle(Particle.ENCHANT, b.getParticleLocation(), 60);
p.getWorld().playSound(p.getLocation(), Sound.BLOCK_ENCHANTMENT_TABLE_USE, 0.7f, 2);
Expand All @@ -106,8 +148,7 @@ public void addGemEvent(PlayerInteractEvent e) {
iBlock.setLocation(loc);
iBlock.setParticleLocation(particleLoc);
iBlock.setInfustionHits(0);
p.getInventory().getItemInMainHand().setAmount(p.getInventory().getItemInMainHand().getAmount()-1);
iBlock.addGem(gem);
if (!deposit(p, iBlock, gem)) return;
p.sendTitle(ChatColor.LIGHT_PURPLE + "Added " + gem.getColour() + gem.getName(), ChatColor.GRAY + "Current Gem Amount: " + ChatColor.YELLOW + iBlock.getCurrentItems().size()+"/10", 1, 40, 20);
iBlock.getLocation().getWorld().spawnParticle(Particle.ENCHANT, iBlock.getParticleLocation(), 60);
p.getWorld().playSound(p.getLocation(), Sound.BLOCK_ENCHANTMENT_TABLE_USE, 0.7f, 2);
Expand All @@ -129,6 +170,7 @@ public void infuseHitEvent(PlayerInteractEvent e) {
InfusionBlock b = currentStations.get(i);
if(b.getLocation().equals(loc)) {
e.setCancelled(true);
if (b.getInfusionHits() >= 5) return;
if(b.getCurrentItems().size() > 0) {
Player p = e.getPlayer();
ItemStack item = p.getInventory().getItemInMainHand();
Expand All @@ -137,68 +179,78 @@ public void infuseHitEvent(PlayerInteractEvent e) {
String itemType = ConfigLoader.infusionStaff.split("\\.")[0];
String itemID = ConfigLoader.infusionStaff.split("\\.")[1];
if(nbt.getType().equalsIgnoreCase(itemType) && nbt.getString("MMOITEMS_ITEM_ID").equalsIgnoreCase(itemID)) {
// Resolve the whole batch before consuming the staff or clearing the bench.
// A later config reload must not strand an already consumed batch.
List<GemRarity> batchRarities = new ArrayList<>();
// Build once while the player's attributes are available; persist the rolled items.
List<InfusionBlock.Drop> prepared = new ArrayList<>();
if (b.getInfusionHits() >= 4) {
try {
for (Gemstone ignored : b.getCurrentItems()) batchRarities.add(getRarity());
} catch (IllegalStateException invalidRarities) {
GoldsmithLog.warn(invalidRarities.getMessage());
p.sendMessage(ChatColor.RED + "Infusion rarities are not configured correctly. Contact staff; your gems remain on the bench.");
for (Gemstone gem : b.getCurrentItems()) {
GemRarity rarity = getRarity();
ItemStack output = Objects.requireNonNull(getInfusedGem(p, gem, b.getCurrentItems().size(), rarity), "Cannot build infused gem");
String announcement = rarity.shouldAnnounce()
? "§e" + p.getName() + " just infused a " + rarity.getName() + " " + gem.getColour() + gem.getName() + "§e Gemstone" : "";
prepared.add(new InfusionBlock.Drop(output, announcement));
}
} catch (RuntimeException invalidBatch) {
GoldsmithLog.warn(invalidBatch.getMessage());
p.sendMessage(ChatColor.RED + "Infusion could not be prepared. Contact staff; your gems remain on the bench.");
return;
}
}
b.setInfustionHits(b.getInfusionHits()+1);
b.pendingDrops.addAll(prepared);
if (!store.save(b)) {
b.setInfustionHits(b.getInfusionHits()-1);
b.pendingDrops.clear();
p.sendMessage(ChatColor.RED + "Infusion progress could not be saved. Contact staff; your gems remain on the bench.");
return;
}
p.sendTitle(ChatColor.LIGHT_PURPLE + "Infusing...", ChatColor.GREEN + "Progress: " + ChatColor.YELLOW + b.getInfusionHits() + "/5", 1, 40, 20);
b.getLocation().getWorld().spawnParticle(Particle.FLAME, b.getParticleLocation(), 60);
b.getLocation().getWorld().spawnParticle(Particle.CRIT, b.getParticleLocation(), 60);
p.getWorld().playSound(p.getLocation(), Sound.BLOCK_AMETHYST_BLOCK_HIT, 1f, 0.2f*b.getInfusionHits());
p.getWorld().playSound(p.getLocation(), Sound.ENTITY_WITHER_SHOOT, 0.2f, 0.2f*b.getInfusionHits());
if(b.getInfusionHits() >= 5) {
currentStations.remove(b);
p.getInventory().getItemInMainHand().setAmount(p.getInventory().getItemInMainHand().getAmount()-1);
b.getParticleLocation().getWorld().spawnEntity(b.getParticleLocation(), EntityType.LIGHTNING_BOLT);
b.getLocation().getWorld().spawnParticle(Particle.FLAME, b.getParticleLocation(), 600);
p.getWorld().playSound(p.getLocation(), Sound.ENTITY_ENDER_DRAGON_GROWL, 1f, 0.7f);
new BukkitRunnable()
{
Integer i = 0;
public void run()
{
if(i < b.getCurrentItems().size()) {
Gemstone gem = b.getCurrentItems().get(i);
GemRarity r = batchRarities.get(i);
if(r.shouldAnnounce()) {
for(Player player : Bukkit.getOnlinePlayers()) {
player.sendMessage("§e"+p.getName()+" just infused a "+r.getName()+" "+gem.getColour()+gem.getName()+"§e Gemstone");
}
}
ItemStack dropGem = getInfusedGem(p, gem, b.getCurrentItems().size(), r);
Location dropLocation = b.getLocation().clone().add(0.5, 1.1, 0.5); // Center-top of the block
Item dropped = p.getWorld().dropItem(dropLocation, dropGem);

// Random upward and outward velocity
Vector velocity = new Vector(
(Math.random() - 0.5) * 0.2, // X: between -0.3 and +0.3
0.2 + Math.random() * 0.2, // Y: between 0.2 and 0.4
(Math.random() - 0.5) * 0.2 // Z: between -0.3 and +0.3
);
dropped.setVelocity(velocity);
p.getWorld().playSound(p.getLocation(), Sound.ENTITY_GLOW_ITEM_FRAME_REMOVE_ITEM, 1f, 1f);
b.getLocation().getWorld().spawnParticle(Particle.FLAME, b.getParticleLocation(), 20);
} else {
this.cancel();
}
i++;
}
}.runTaskTimer(InfusionMain.plugin, 30L, 2L);
scheduleDrops(b);
}
}
}
}
}
}
private void scheduleDrops(InfusionBlock station) {
BukkitRunnable delivery = new BukkitRunnable() {
@Override
public void run() {
// Retry failed checkpoints before delivering another item.
if (!store.save(station)) return;
if (station.pendingDrops.isEmpty()) {
if (!store.delete(station)) return;
currentStations.remove(station);
deliveries.remove(this);
cancel();
return;
}
InfusionBlock.Drop output = station.pendingDrops.getFirst();
Comment thread
ryanbarlow97 marked this conversation as resolved.
Resolved
Location dropLocation = station.getLocation().clone().add(0.5, 1.1, 0.5);
Item dropped = dropLocation.getWorld().dropItem(dropLocation, output.item());
station.pendingDrops.removeFirst();
store.save(station);
if (!output.announcement().isEmpty()) {
for (Player player : Bukkit.getOnlinePlayers()) player.sendMessage(output.announcement());
}
dropped.setVelocity(new Vector((Math.random() - 0.5) * 0.2, 0.2 + Math.random() * 0.2, (Math.random() - 0.5) * 0.2));
dropLocation.getWorld().playSound(dropLocation, Sound.ENTITY_GLOW_ITEM_FRAME_REMOVE_ITEM, 1f, 1f);
dropLocation.getWorld().spawnParticle(Particle.FLAME, station.getParticleLocation(), 20);
}
};
delivery.runTaskTimer(InfusionMain.plugin, 30L, 2L);
deliveries.add(delivery);
}

public GemRarity getRarity() {
return getRarity(Math.random());
}
Expand Down
7 changes: 6 additions & 1 deletion src/main/java/net/tfminecraft/geminfusion/InfusionMain.java
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public void onEnable() {
config = getConfig();
loader.loadConfig(config);
loadGoldsmithConfigs();
events.loadPersisted();

getServer().getPluginManager().registerEvents(events, this);
getServer().getPluginManager().registerEvents(goldsmithStations, this);
Expand All @@ -56,12 +57,16 @@ public void onEnable() {
getCommand(commands.cmd1).setExecutor(commands);
getCommand(commands.cmd1).setTabCompleter(commands);
goldsmithStations.loadPersisted();
getServer().getScheduler().runTaskTimer(this, () -> goldsmithStations.flush(false), 20L * 60, 20L * 60);
getServer().getScheduler().runTaskTimer(this, () -> {
events.flush();
goldsmithStations.flush(false);
}, 20L * 60, 20L * 60);
GoldsmithLog.info("Loaded " + JewelryProjectLoader.get().size() + " goldsmithing projects");
}

@Override
public void onDisable() {
events.shutdown();
goldsmithStations.flush(true);
}

Expand Down
143 changes: 143 additions & 0 deletions src/main/java/net/tfminecraft/geminfusion/InfusionStationStore.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package net.tfminecraft.geminfusion;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;

import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.configuration.InvalidConfigurationException;
import org.bukkit.configuration.file.YamlConfiguration;

import net.tfminecraft.geminfusion.goldsmith.GoldsmithLog;

/** One atomic snapshot per infusion table; rejected snapshots remain untouched. */
final class InfusionStationStore {
private final Set<Path> rejected = new HashSet<>();

Path folder() {
return InfusionMain.plugin.getDataFolder().toPath().resolve("data/infusion-stations");
}

Path fileFor(Location location) {
return folder().resolve(location.getWorld().getUID() + "_" + location.getBlockX() + "_"
+ location.getBlockY() + "_" + location.getBlockZ() + ".yml");
}

List<InfusionBlock> loadAll() {
List<InfusionBlock> stations = new ArrayList<>();
rejected.clear();
try {
Files.createDirectories(folder());
try (var paths = Files.list(folder())) {
for (Path file : paths.filter(path -> path.toString().endsWith(".yml")).toList()) {
try {
stations.add(read(file));
} catch (IOException | InvalidConfigurationException | IllegalArgumentException | NullPointerException ex) {
rejected.add(file);
GoldsmithLog.warn("Cannot load infusion table " + file.getFileName()
+ "; retaining its saved gems: " + ex.getMessage());
}
}
}
} catch (IOException ex) {
throw new IllegalStateException("Cannot read infusion tables; refusing to accept gems", ex);
}
return stations;
}

boolean save(InfusionBlock station) {
Path file = fileFor(station.getLocation());
boolean replaced = false;
try {
if (rejected.contains(file)) throw new IOException("Repair the retained snapshot before using this table");
YamlConfiguration yaml = new YamlConfiguration();
Location location = station.getLocation();
yaml.set("world", location.getWorld().getName());
yaml.set("x", location.getBlockX());
yaml.set("y", location.getBlockY());
yaml.set("z", location.getBlockZ());
yaml.set("particle-x", station.getParticleLocation().getX());
yaml.set("particle-y", station.getParticleLocation().getY());
yaml.set("particle-z", station.getParticleLocation().getZ());
yaml.set("hits", station.getInfusionHits());
yaml.set("gems", station.getCurrentItems().stream().map(Gemstone::getId).toList());
for (int i = 0; i < station.pendingDrops.size(); i++) {
InfusionBlock.Drop drop = station.pendingDrops.get(i);
yaml.set("drops." + i + ".item", drop.item());
yaml.set("drops." + i + ".announcement", drop.announcement());
}
Files.createDirectories(folder());
Path temporary = Files.createTempFile(folder(), "infusion-", ".tmp");
try {
ByteBuffer bytes = ByteBuffer.wrap(yaml.saveToString().getBytes(StandardCharsets.UTF_8));
try (FileChannel channel = FileChannel.open(temporary, StandardOpenOption.WRITE)) {
while (bytes.hasRemaining()) channel.write(bytes);
channel.force(true);
}
Files.move(temporary, file, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
replaced = true;
try (FileChannel directory = FileChannel.open(folder(), StandardOpenOption.READ)) {
directory.force(true);
}
} finally {
Files.deleteIfExists(temporary);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
return true;
} catch (IOException | IllegalArgumentException ex) {
GoldsmithLog.warn("Cannot save infusion table " + file.getFileName() + ": " + ex.getMessage());
// A post-rename sync failure must not roll back items already recorded on disk.
return replaced;
}
}

boolean delete(InfusionBlock station) {
try {
Files.deleteIfExists(fileFor(station.getLocation()));
return true;
} catch (IOException ex) {
GoldsmithLog.warn("Cannot delete finished infusion table: " + ex.getMessage());
return false;
}
}

private InfusionBlock read(Path file) throws IOException, InvalidConfigurationException {
YamlConfiguration yaml = new YamlConfiguration();
yaml.load(file.toFile());
World world = Objects.requireNonNull(Bukkit.getWorld(Objects.requireNonNull(yaml.getString("world"))), "Missing world");
InfusionBlock station = new InfusionBlock();
station.setLocation(new Location(world, yaml.getInt("x"), yaml.getInt("y"), yaml.getInt("z")));
if (!file.equals(fileFor(station.getLocation()))) throw new IllegalArgumentException("Table location does not match filename");
station.setParticleLocation(new Location(world, yaml.getDouble("particle-x"), yaml.getDouble("particle-y"), yaml.getDouble("particle-z")));
int hits = yaml.getInt("hits", -1);
if (hits < 0 || hits > 5) throw new IllegalArgumentException("Invalid infusion progress");
station.setInfustionHits(hits);
for (String id : yaml.getStringList("gems")) {
Gemstone gem = ConfigLoader.loadedGems.stream().filter(candidate -> candidate.getId().equals(id)).findFirst().orElseThrow(
() -> new IllegalArgumentException("Unknown gem " + id));
station.addGem(gem);
}
if (station.currentGems.isEmpty() || station.currentGems.size() > 10) throw new IllegalArgumentException("Invalid gem count");
if (yaml.isConfigurationSection("drops")) {
for (String key : yaml.getConfigurationSection("drops").getKeys(false)) {
station.pendingDrops.add(new InfusionBlock.Drop(
Objects.requireNonNull(yaml.getItemStack("drops." + key + ".item"), "Missing infused gem"),
yaml.getString("drops." + key + ".announcement", "")));
}
}
if (station.pendingDrops.size() > station.currentGems.size()
|| (hits < 5 && !station.pendingDrops.isEmpty())) throw new IllegalArgumentException("Invalid pending output");
return station;
}
}
Loading